feat: Introduce core bot components, on-chain wallet watchers, Telegram notifications, and configuration management.
This commit is contained in:
+4
-1
@@ -1,6 +1,9 @@
|
|||||||
# Telegram Bot
|
# Telegram Bot
|
||||||
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
||||||
TELEGRAM_CHAT_ID=your_chat_id_here
|
TELEGRAM_CHAT_ID=your_chat_id_here
|
||||||
|
# Optional multi-chat target (comma-separated). If set, it will be merged with TELEGRAM_CHAT_ID.
|
||||||
|
# Example: TELEGRAM_CHAT_IDS=-1003586303099,-1003539418691
|
||||||
|
TELEGRAM_CHAT_IDS=
|
||||||
TELEGRAM_ALERT_PUSH_ENABLED=true
|
TELEGRAM_ALERT_PUSH_ENABLED=true
|
||||||
TELEGRAM_ALERT_PUSH_INTERVAL_SEC=300
|
TELEGRAM_ALERT_PUSH_INTERVAL_SEC=300
|
||||||
TELEGRAM_ALERT_PUSH_COOLDOWN_SEC=1800
|
TELEGRAM_ALERT_PUSH_COOLDOWN_SEC=1800
|
||||||
@@ -50,7 +53,7 @@ NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
|
|||||||
NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com
|
NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com
|
||||||
# Bot command access guard (/city, /deb):
|
# Bot command access guard (/city, /deb):
|
||||||
# - Pro entitlement removed
|
# - Pro entitlement removed
|
||||||
# - user only needs to be a member of TELEGRAM_CHAT_ID group
|
# - user only needs to be a member of any configured group (TELEGRAM_CHAT_IDS / TELEGRAM_CHAT_ID)
|
||||||
POLYWEATHER_BOT_GROUP_INVITE_URL=
|
POLYWEATHER_BOT_GROUP_INVITE_URL=
|
||||||
# Group message points (anti-spam + ranking)
|
# Group message points (anti-spam + ranking)
|
||||||
POLYWEATHER_BOT_MESSAGE_POINTS=4
|
POLYWEATHER_BOT_MESSAGE_POINTS=4
|
||||||
|
|||||||
+34
-24
@@ -6,6 +6,7 @@ from typing import Any
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from src.bot.io_layer import BotIOLayer
|
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:
|
class CommandGuard:
|
||||||
@@ -15,7 +16,10 @@ class CommandGuard:
|
|||||||
|
|
||||||
def __init__(self, io_layer: BotIOLayer, group_chat_id: str | None = None):
|
def __init__(self, io_layer: BotIOLayer, group_chat_id: str | None = None):
|
||||||
self.io_layer = io_layer
|
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()
|
self.group_invite_url = str(os.getenv("POLYWEATHER_BOT_GROUP_INVITE_URL") or "").strip()
|
||||||
|
|
||||||
def _reply_group_required(self, message: Any) -> None:
|
def _reply_group_required(self, message: Any) -> None:
|
||||||
@@ -30,42 +34,48 @@ class CommandGuard:
|
|||||||
if user_id is None:
|
if user_id is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not self.group_chat_id:
|
if not self.group_chat_ids:
|
||||||
logger.error(
|
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,
|
command_label,
|
||||||
user_id,
|
user_id,
|
||||||
)
|
)
|
||||||
self.io_layer.bot.reply_to(
|
self.io_layer.bot.reply_to(
|
||||||
message,
|
message,
|
||||||
"⚠️ 机器人未配置群组准入(TELEGRAM_CHAT_ID),请联系管理员。",
|
"⚠️ 机器人未配置群组准入(TELEGRAM_CHAT_IDS / TELEGRAM_CHAT_ID),请联系管理员。",
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
blocked_statuses: list[str] = []
|
||||||
member = self.io_layer.bot.get_chat_member(self.group_chat_id, int(user_id))
|
lookup_failures: list[str] = []
|
||||||
status = str(getattr(member, "status", "") or "").strip().lower()
|
for chat_id in self.group_chat_ids:
|
||||||
except Exception as exc:
|
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(
|
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,
|
command_label,
|
||||||
user_id,
|
user_id,
|
||||||
self.group_chat_id,
|
",".join(self.group_chat_ids),
|
||||||
exc,
|
"|".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)
|
self._reply_group_required(message)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from src.bot.io_layer import BotIOLayer
|
|||||||
from src.bot.runtime_coordinator import StartupCoordinator
|
from src.bot.runtime_coordinator import StartupCoordinator
|
||||||
from src.bot.services.city_command_service import CityCommandService
|
from src.bot.services.city_command_service import CityCommandService
|
||||||
from src.bot.services.deb_command_service import DebCommandService
|
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:
|
def _project_root() -> str:
|
||||||
@@ -68,7 +69,7 @@ def start_bot() -> None:
|
|||||||
config=config,
|
config=config,
|
||||||
command_access_mode="group_member_only",
|
command_access_mode="group_member_only",
|
||||||
protected_commands=["/city", "/deb"],
|
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(
|
_register_handlers(
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from datetime import datetime
|
|||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
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:
|
def _env_bool(name: str, default: bool) -> bool:
|
||||||
raw = os.getenv(name)
|
raw = os.getenv(name)
|
||||||
@@ -148,7 +150,7 @@ class StartupCoordinator:
|
|||||||
|
|
||||||
def _start_trade_alert_loop(self) -> LoopStatus:
|
def _start_trade_alert_loop(self) -> LoopStatus:
|
||||||
enabled = _env_bool("TELEGRAM_ALERT_PUSH_ENABLED", True)
|
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)
|
mispricing_only = _env_bool("TELEGRAM_ALERT_MISPRICING_ONLY", True)
|
||||||
interval = (
|
interval = (
|
||||||
max(300, _env_int("TELEGRAM_ALERT_MISPRICING_INTERVAL_SEC", 7200))
|
max(300, _env_int("TELEGRAM_ALERT_MISPRICING_INTERVAL_SEC", 7200))
|
||||||
@@ -160,8 +162,9 @@ class StartupCoordinator:
|
|||||||
"mode": "mispricing-only" if mispricing_only else "full",
|
"mode": "mispricing-only" if mispricing_only else "full",
|
||||||
"interval_sec": interval,
|
"interval_sec": interval,
|
||||||
"cities_count": cities_count,
|
"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(
|
return self._start_with_validation(
|
||||||
key="trade_alert_push",
|
key="trade_alert_push",
|
||||||
label="错价雷达推送",
|
label="错价雷达推送",
|
||||||
@@ -176,7 +179,7 @@ class StartupCoordinator:
|
|||||||
|
|
||||||
def _start_polygon_wallet_loop(self) -> LoopStatus:
|
def _start_polygon_wallet_loop(self) -> LoopStatus:
|
||||||
enabled = _env_bool("POLYGON_WALLET_WATCH_ENABLED", False)
|
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()
|
rpc_url = str(os.getenv("POLYGON_RPC_URL") or "").strip()
|
||||||
wallets_count = _parse_csv_count(os.getenv("POLYGON_WALLET_WATCH_ADDRESSES"))
|
wallets_count = _parse_csv_count(os.getenv("POLYGON_WALLET_WATCH_ADDRESSES"))
|
||||||
poll = max(3, _env_int("POLYGON_WALLET_WATCH_INTERVAL_SEC", 8))
|
poll = max(3, _env_int("POLYGON_WALLET_WATCH_INTERVAL_SEC", 8))
|
||||||
@@ -184,10 +187,11 @@ class StartupCoordinator:
|
|||||||
"poll_sec": poll,
|
"poll_sec": poll,
|
||||||
"wallets_count": wallets_count,
|
"wallets_count": wallets_count,
|
||||||
"polymarket_only": _env_bool("POLYGON_WALLET_WATCH_POLYMARKET_ONLY", True),
|
"polymarket_only": _env_bool("POLYGON_WALLET_WATCH_POLYMARKET_ONLY", True),
|
||||||
|
"chat_targets": len(chat_ids),
|
||||||
}
|
}
|
||||||
validation_error = None
|
validation_error = None
|
||||||
if not chat_id:
|
if not chat_ids:
|
||||||
validation_error = "missing_TELEGRAM_CHAT_ID"
|
validation_error = "missing_TELEGRAM_CHAT_IDS"
|
||||||
elif not rpc_url:
|
elif not rpc_url:
|
||||||
validation_error = "missing_POLYGON_RPC_URL"
|
validation_error = "missing_POLYGON_RPC_URL"
|
||||||
elif wallets_count == 0:
|
elif wallets_count == 0:
|
||||||
@@ -205,17 +209,18 @@ class StartupCoordinator:
|
|||||||
|
|
||||||
def _start_polymarket_wallet_activity_loop(self) -> LoopStatus:
|
def _start_polymarket_wallet_activity_loop(self) -> LoopStatus:
|
||||||
enabled = _env_bool("POLYMARKET_WALLET_ACTIVITY_ENABLED", False)
|
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"))
|
users_count = _parse_csv_count(os.getenv("POLYMARKET_WALLET_ACTIVITY_USERS"))
|
||||||
poll = max(5, _env_int("POLYMARKET_WALLET_ACTIVITY_INTERVAL_SEC", 20))
|
poll = max(5, _env_int("POLYMARKET_WALLET_ACTIVITY_INTERVAL_SEC", 20))
|
||||||
details = {
|
details = {
|
||||||
"poll_sec": poll,
|
"poll_sec": poll,
|
||||||
"users_count": users_count,
|
"users_count": users_count,
|
||||||
"link_preview": _env_bool("POLYMARKET_WALLET_ACTIVITY_LINK_PREVIEW", True),
|
"link_preview": _env_bool("POLYMARKET_WALLET_ACTIVITY_LINK_PREVIEW", True),
|
||||||
|
"chat_targets": len(chat_ids),
|
||||||
}
|
}
|
||||||
validation_error = None
|
validation_error = None
|
||||||
if not chat_id:
|
if not chat_ids:
|
||||||
validation_error = "missing_TELEGRAM_CHAT_ID"
|
validation_error = "missing_TELEGRAM_CHAT_IDS"
|
||||||
elif users_count == 0:
|
elif users_count == 0:
|
||||||
validation_error = "missing_POLYMARKET_WALLET_ACTIVITY_USERS"
|
validation_error = "missing_POLYMARKET_WALLET_ACTIVITY_USERS"
|
||||||
return self._start_with_validation(
|
return self._start_with_validation(
|
||||||
@@ -231,7 +236,7 @@ class StartupCoordinator:
|
|||||||
|
|
||||||
def _start_weekly_reward_loop(self) -> LoopStatus:
|
def _start_weekly_reward_loop(self) -> LoopStatus:
|
||||||
enabled = _env_bool("POLYWEATHER_WEEKLY_REWARD_ENABLED", True)
|
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(
|
settle_weekday = min(
|
||||||
7, max(1, _env_int("POLYWEATHER_WEEKLY_REWARD_SETTLE_WEEKDAY", 1))
|
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}",
|
"settle_time": f"{settle_hour:02d}:{settle_minute:02d}",
|
||||||
"check_interval_sec": check_interval,
|
"check_interval_sec": check_interval,
|
||||||
"announce": _env_bool("POLYWEATHER_WEEKLY_REWARD_ANNOUNCE_ENABLED", True),
|
"announce": _env_bool("POLYWEATHER_WEEKLY_REWARD_ANNOUNCE_ENABLED", True),
|
||||||
|
"chat_targets": len(chat_ids),
|
||||||
}
|
}
|
||||||
announce_enabled = bool(details["announce"])
|
announce_enabled = bool(details["announce"])
|
||||||
validation_error = None
|
validation_error = None
|
||||||
if announce_enabled and not chat_id:
|
if announce_enabled and not chat_ids:
|
||||||
validation_error = "missing_TELEGRAM_CHAT_ID"
|
validation_error = "missing_TELEGRAM_CHAT_IDS"
|
||||||
return self._start_with_validation(
|
return self._start_with_validation(
|
||||||
key="weekly_reward",
|
key="weekly_reward",
|
||||||
label="周榜奖励结算",
|
label="周榜奖励结算",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import requests
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from src.database.db_manager import DBManager
|
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:
|
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)
|
interval_sec = _env_int("POLYWEATHER_WEEKLY_REWARD_CHECK_INTERVAL_SEC", 300, 30)
|
||||||
timeout_sec = _env_int("POLYWEATHER_WEEKLY_REWARD_HTTP_TIMEOUT_SEC", 10, 3)
|
timeout_sec = _env_int("POLYWEATHER_WEEKLY_REWARD_HTTP_TIMEOUT_SEC", 10, 3)
|
||||||
announce = _env_bool("POLYWEATHER_WEEKLY_REWARD_ANNOUNCE_ENABLED", True)
|
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("/")
|
supabase_url = str(os.getenv("SUPABASE_URL") or "").strip().rstrip("/")
|
||||||
service_role_key = str(os.getenv("SUPABASE_SERVICE_ROLE_KEY") or "").strip()
|
service_role_key = str(os.getenv("SUPABASE_SERVICE_ROLE_KEY") or "").strip()
|
||||||
|
|
||||||
db = DBManager()
|
db = DBManager()
|
||||||
logger.info(
|
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,
|
tz_name,
|
||||||
settle_weekday,
|
settle_weekday,
|
||||||
settle_hour,
|
settle_hour,
|
||||||
settle_minute,
|
settle_minute,
|
||||||
interval_sec,
|
interval_sec,
|
||||||
announce,
|
announce,
|
||||||
|
len(chat_ids),
|
||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@@ -313,16 +315,22 @@ def _runner(bot: Any) -> None:
|
|||||||
len(winners),
|
len(winners),
|
||||||
skipped,
|
skipped,
|
||||||
)
|
)
|
||||||
if announce and chat_id:
|
if announce and chat_ids:
|
||||||
try:
|
message = _render_settle_report(week_key=week_key, winners=winners, skipped=skipped)
|
||||||
bot.send_message(
|
for chat_id in chat_ids:
|
||||||
chat_id,
|
try:
|
||||||
_render_settle_report(week_key=week_key, winners=winners, skipped=skipped),
|
bot.send_message(
|
||||||
parse_mode="HTML",
|
chat_id,
|
||||||
disable_web_page_preview=True,
|
message,
|
||||||
)
|
parse_mode="HTML",
|
||||||
except Exception as exc:
|
disable_web_page_preview=True,
|
||||||
logger.warning(f"weekly reward announcement failed: {exc}")
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(
|
||||||
|
"weekly reward announcement failed chat_id={} error={}",
|
||||||
|
chat_id,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(f"weekly reward cycle failed: {exc}")
|
logger.warning(f"weekly reward cycle failed: {exc}")
|
||||||
time.sleep(interval_sec)
|
time.sleep(interval_sec)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from typing import Any, Dict, List, Optional, Set, Tuple
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from web3 import Web3
|
from web3 import Web3
|
||||||
|
|
||||||
|
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
|
||||||
|
|
||||||
TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
||||||
APPROVAL_TOPIC = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"
|
APPROVAL_TOPIC = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"
|
||||||
ERC20_ABI = [
|
ERC20_ABI = [
|
||||||
@@ -372,7 +374,7 @@ def _build_message(
|
|||||||
|
|
||||||
def start_polygon_wallet_watch_loop(bot: Any) -> Optional[threading.Thread]:
|
def start_polygon_wallet_watch_loop(bot: Any) -> Optional[threading.Thread]:
|
||||||
enabled = _env_bool("POLYGON_WALLET_WATCH_ENABLED", False)
|
enabled = _env_bool("POLYGON_WALLET_WATCH_ENABLED", False)
|
||||||
chat_id = os.getenv("TELEGRAM_CHAT_ID")
|
chat_ids = get_telegram_chat_ids_from_env()
|
||||||
rpc_url = os.getenv("POLYGON_RPC_URL")
|
rpc_url = os.getenv("POLYGON_RPC_URL")
|
||||||
watch_set = _parse_addresses(os.getenv("POLYGON_WALLET_WATCH_ADDRESSES"))
|
watch_set = _parse_addresses(os.getenv("POLYGON_WALLET_WATCH_ADDRESSES"))
|
||||||
polymarket_only = _env_bool("POLYGON_WALLET_WATCH_POLYMARKET_ONLY", True)
|
polymarket_only = _env_bool("POLYGON_WALLET_WATCH_POLYMARKET_ONLY", True)
|
||||||
@@ -381,8 +383,8 @@ def start_polygon_wallet_watch_loop(bot: Any) -> Optional[threading.Thread]:
|
|||||||
if not enabled:
|
if not enabled:
|
||||||
logger.info("polygon wallet watcher disabled")
|
logger.info("polygon wallet watcher disabled")
|
||||||
return None
|
return None
|
||||||
if not chat_id:
|
if not chat_ids:
|
||||||
logger.warning("polygon wallet watcher skipped: TELEGRAM_CHAT_ID is not set")
|
logger.warning("polygon wallet watcher skipped: TELEGRAM_CHAT_IDS is not set")
|
||||||
return None
|
return None
|
||||||
if not rpc_url:
|
if not rpc_url:
|
||||||
logger.warning("polygon wallet watcher skipped: POLYGON_RPC_URL is not set")
|
logger.warning("polygon wallet watcher skipped: POLYGON_RPC_URL is not set")
|
||||||
@@ -426,7 +428,8 @@ def start_polygon_wallet_watch_loop(bot: Any) -> Optional[threading.Thread]:
|
|||||||
logger.info(
|
logger.info(
|
||||||
f"polygon wallet watcher started wallets={len(watch_set)} "
|
f"polygon wallet watcher started wallets={len(watch_set)} "
|
||||||
f"polymarket_only={polymarket_only} pm_contracts={len(pm_contracts)} "
|
f"polymarket_only={polymarket_only} pm_contracts={len(pm_contracts)} "
|
||||||
f"poll={poll_sec}s confirmations={confirmations} state_path={state_path}"
|
f"poll={poll_sec}s confirmations={confirmations} chat_targets={len(chat_ids)} "
|
||||||
|
f"state_path={state_path}"
|
||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@@ -503,11 +506,28 @@ def start_polygon_wallet_watch_loop(bot: Any) -> Optional[threading.Thread]:
|
|||||||
approval_lines=approval_lines,
|
approval_lines=approval_lines,
|
||||||
tx_to_label=tx_to_label,
|
tx_to_label=tx_to_label,
|
||||||
)
|
)
|
||||||
bot.send_message(chat_id, message, disable_web_page_preview=True)
|
sent_count = 0
|
||||||
|
for chat_id in chat_ids:
|
||||||
|
try:
|
||||||
|
bot.send_message(
|
||||||
|
chat_id,
|
||||||
|
message,
|
||||||
|
disable_web_page_preview=True,
|
||||||
|
)
|
||||||
|
sent_count += 1
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(
|
||||||
|
"polygon wallet alert push failed wallet={} chat_id={} error={}",
|
||||||
|
matched_wallet,
|
||||||
|
chat_id,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
if sent_count <= 0:
|
||||||
|
continue
|
||||||
state.setdefault("seen_tx", {})[tx_hash] = cycle_ts
|
state.setdefault("seen_tx", {})[tx_hash] = cycle_ts
|
||||||
logger.info(
|
logger.info(
|
||||||
f"polygon wallet alert pushed wallet={matched_wallet} "
|
f"polygon wallet alert pushed wallet={matched_wallet} "
|
||||||
f"tx={tx_hash} block={block_num} polymarket={pm_hit}"
|
f"tx={tx_hash} block={block_num} polymarket={pm_hit} chat_targets={sent_count}"
|
||||||
)
|
)
|
||||||
|
|
||||||
state["last_scanned_block"] = block_num
|
state["last_scanned_block"] = block_num
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
import requests
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
|
||||||
|
|
||||||
|
|
||||||
def _env_bool(name: str, default: bool) -> bool:
|
def _env_bool(name: str, default: bool) -> bool:
|
||||||
raw = os.getenv(name)
|
raw = os.getenv(name)
|
||||||
@@ -547,7 +549,7 @@ def _filter_changes_by_position_value(
|
|||||||
|
|
||||||
def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread]:
|
def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread]:
|
||||||
enabled = _env_bool("POLYMARKET_WALLET_ACTIVITY_ENABLED", False)
|
enabled = _env_bool("POLYMARKET_WALLET_ACTIVITY_ENABLED", False)
|
||||||
chat_id = os.getenv("TELEGRAM_CHAT_ID")
|
chat_ids = get_telegram_chat_ids_from_env()
|
||||||
users = _parse_addresses(os.getenv("POLYMARKET_WALLET_ACTIVITY_USERS"))
|
users = _parse_addresses(os.getenv("POLYMARKET_WALLET_ACTIVITY_USERS"))
|
||||||
user_aliases = _parse_address_aliases(
|
user_aliases = _parse_address_aliases(
|
||||||
os.getenv("POLYMARKET_WALLET_ACTIVITY_USER_ALIASES")
|
os.getenv("POLYMARKET_WALLET_ACTIVITY_USER_ALIASES")
|
||||||
@@ -560,8 +562,8 @@ def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread
|
|||||||
if not enabled:
|
if not enabled:
|
||||||
logger.info("polymarket wallet activity watcher disabled")
|
logger.info("polymarket wallet activity watcher disabled")
|
||||||
return None
|
return None
|
||||||
if not chat_id:
|
if not chat_ids:
|
||||||
logger.warning("polymarket wallet activity watcher skipped: TELEGRAM_CHAT_ID is not set")
|
logger.warning("polymarket wallet activity watcher skipped: TELEGRAM_CHAT_IDS is not set")
|
||||||
return None
|
return None
|
||||||
if not users:
|
if not users:
|
||||||
logger.warning("polymarket wallet activity watcher skipped: POLYMARKET_WALLET_ACTIVITY_USERS is empty")
|
logger.warning("polymarket wallet activity watcher skipped: POLYMARKET_WALLET_ACTIVITY_USERS is empty")
|
||||||
@@ -627,6 +629,7 @@ def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread
|
|||||||
f"poll={poll_sec}s data_api={data_api_url} price_filter={min_price}-{max_price} "
|
f"poll={poll_sec}s data_api={data_api_url} price_filter={min_price}-{max_price} "
|
||||||
f"min_position_value_usd={min_position_value_usd} "
|
f"min_position_value_usd={min_position_value_usd} "
|
||||||
f"min_value_exempt_users={len(exempt_wallets)} "
|
f"min_value_exempt_users={len(exempt_wallets)} "
|
||||||
|
f"chat_targets={len(chat_ids)} "
|
||||||
f"aliases={len(user_aliases)} link_preview={link_preview} "
|
f"aliases={len(user_aliases)} link_preview={link_preview} "
|
||||||
f"min_avg_price_delta={min_avg_price_delta} "
|
f"min_avg_price_delta={min_avg_price_delta} "
|
||||||
f"immediate_on_size_delta={immediate_on_size_delta} "
|
f"immediate_on_size_delta={immediate_on_size_delta} "
|
||||||
@@ -770,13 +773,26 @@ def start_polymarket_wallet_activity_loop(bot: Any) -> Optional[threading.Thread
|
|||||||
max_changes=max_changes,
|
max_changes=max_changes,
|
||||||
wallet_alias=user_aliases.get(user),
|
wallet_alias=user_aliases.get(user),
|
||||||
)
|
)
|
||||||
bot.send_message(
|
sent_count = 0
|
||||||
chat_id,
|
for chat_id in chat_ids:
|
||||||
msg,
|
try:
|
||||||
disable_web_page_preview=not link_preview,
|
bot.send_message(
|
||||||
)
|
chat_id,
|
||||||
|
msg,
|
||||||
|
disable_web_page_preview=not link_preview,
|
||||||
|
)
|
||||||
|
sent_count += 1
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(
|
||||||
|
"wallet activity push failed user={} chat_id={} error={}",
|
||||||
|
user,
|
||||||
|
chat_id,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
if sent_count <= 0:
|
||||||
|
continue
|
||||||
logger.info(
|
logger.info(
|
||||||
f"wallet activity pushed user={user} changes={len(outgoing)}"
|
f"wallet activity pushed user={user} changes={len(outgoing)} chat_targets={sent_count}"
|
||||||
)
|
)
|
||||||
|
|
||||||
users_state[user] = {
|
users_state[user] = {
|
||||||
|
|||||||
@@ -1508,8 +1508,13 @@ class PaymentContractCheckoutService:
|
|||||||
if not self.notify_telegram:
|
if not self.notify_telegram:
|
||||||
return
|
return
|
||||||
token = str(os.getenv("TELEGRAM_BOT_TOKEN") or "").strip()
|
token = str(os.getenv("TELEGRAM_BOT_TOKEN") or "").strip()
|
||||||
chat_id = str(os.getenv("TELEGRAM_CHAT_ID") or "").strip()
|
if not token:
|
||||||
if not token or not chat_id:
|
return
|
||||||
|
user = self._db.get_user_by_supabase_user_id(user_id)
|
||||||
|
if not isinstance(user, dict):
|
||||||
|
return
|
||||||
|
telegram_id = int(user.get("telegram_id") or 0)
|
||||||
|
if telegram_id <= 0:
|
||||||
return
|
return
|
||||||
short_hash = tx_hash[:10] + "..." + tx_hash[-8:] if len(tx_hash) > 20 else tx_hash
|
short_hash = tx_hash[:10] + "..." + tx_hash[-8:] if len(tx_hash) > 20 else tx_hash
|
||||||
text = (
|
text = (
|
||||||
@@ -1523,7 +1528,7 @@ class PaymentContractCheckoutService:
|
|||||||
requests.post(
|
requests.post(
|
||||||
f"https://api.telegram.org/bot{token}/sendMessage",
|
f"https://api.telegram.org/bot{token}/sendMessage",
|
||||||
json={
|
json={
|
||||||
"chat_id": chat_id,
|
"chat_id": str(telegram_id),
|
||||||
"text": text,
|
"text": text,
|
||||||
"disable_web_page_preview": True,
|
"disable_web_page_preview": True,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from src.utils.telegram_chat_ids import (
|
||||||
|
get_primary_telegram_chat_id_from_env,
|
||||||
|
get_telegram_chat_ids_from_env,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
"""
|
"""
|
||||||
@@ -23,7 +28,8 @@ def load_config():
|
|||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"bot_token": os.getenv("TELEGRAM_BOT_TOKEN"),
|
"bot_token": os.getenv("TELEGRAM_BOT_TOKEN"),
|
||||||
"chat_id": os.getenv("TELEGRAM_CHAT_ID"),
|
"chat_id": get_primary_telegram_chat_id_from_env(),
|
||||||
|
"chat_ids": get_telegram_chat_ids_from_env(),
|
||||||
"proxy": os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY"),
|
"proxy": os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY"),
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def _split_chat_ids(raw: str | None) -> List[str]:
|
||||||
|
if not raw:
|
||||||
|
return []
|
||||||
|
normalized = str(raw).replace("\r", ",").replace("\n", ",").replace(";", ",")
|
||||||
|
out: List[str] = []
|
||||||
|
for token in normalized.split(","):
|
||||||
|
value = token.strip()
|
||||||
|
if value:
|
||||||
|
out.append(value)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def parse_telegram_chat_ids(*raw_values: str | None) -> List[str]:
|
||||||
|
"""Parse and de-duplicate chat ids while keeping original order."""
|
||||||
|
out: List[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for raw in raw_values:
|
||||||
|
for chat_id in _split_chat_ids(raw):
|
||||||
|
if chat_id in seen:
|
||||||
|
continue
|
||||||
|
seen.add(chat_id)
|
||||||
|
out.append(chat_id)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def get_telegram_chat_ids_from_env() -> List[str]:
|
||||||
|
"""
|
||||||
|
Preferred env is TELEGRAM_CHAT_IDS (comma-separated).
|
||||||
|
TELEGRAM_CHAT_ID is kept for backward compatibility.
|
||||||
|
"""
|
||||||
|
return parse_telegram_chat_ids(
|
||||||
|
os.getenv("TELEGRAM_CHAT_IDS"),
|
||||||
|
os.getenv("TELEGRAM_CHAT_ID"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_primary_telegram_chat_id_from_env() -> str:
|
||||||
|
chat_ids = get_telegram_chat_ids_from_env()
|
||||||
|
return chat_ids[0] if chat_ids else ""
|
||||||
@@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from src.data_collection.city_registry import CITY_REGISTRY
|
from src.data_collection.city_registry import CITY_REGISTRY
|
||||||
|
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
|
||||||
|
|
||||||
|
|
||||||
SEVERITY_RANK = {
|
SEVERITY_RANK = {
|
||||||
@@ -516,7 +517,7 @@ def build_trade_alert_for_city(
|
|||||||
|
|
||||||
def _maybe_send_alert(
|
def _maybe_send_alert(
|
||||||
bot: Any,
|
bot: Any,
|
||||||
chat_id: str,
|
chat_ids: List[str],
|
||||||
city: str,
|
city: str,
|
||||||
alert_payload: Dict[str, Any],
|
alert_payload: Dict[str, Any],
|
||||||
state: Dict[str, Any],
|
state: Dict[str, Any],
|
||||||
@@ -552,6 +553,9 @@ def _maybe_send_alert(
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if not chat_ids:
|
||||||
|
return False
|
||||||
|
|
||||||
signature = _alert_signature(alert_payload)
|
signature = _alert_signature(alert_payload)
|
||||||
trigger_key = _trigger_type_key(alert_payload)
|
trigger_key = _trigger_type_key(alert_payload)
|
||||||
last_city_sig = last_city.get("signature")
|
last_city_sig = last_city.get("signature")
|
||||||
@@ -568,7 +572,21 @@ def _maybe_send_alert(
|
|||||||
if last_sig_ts and now_ts - last_sig_ts < cooldown_sec:
|
if last_sig_ts and now_ts - last_sig_ts < cooldown_sec:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
bot.send_message(chat_id, message)
|
sent_count = 0
|
||||||
|
for chat_id in chat_ids:
|
||||||
|
try:
|
||||||
|
bot.send_message(chat_id, message)
|
||||||
|
sent_count += 1
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(
|
||||||
|
"trade alert push failed city={} chat_id={} error={}",
|
||||||
|
city,
|
||||||
|
chat_id,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
if sent_count <= 0:
|
||||||
|
return False
|
||||||
|
|
||||||
last_by_city[city] = {
|
last_by_city[city] = {
|
||||||
"signature": signature,
|
"signature": signature,
|
||||||
"trigger_key": trigger_key,
|
"trigger_key": trigger_key,
|
||||||
@@ -581,19 +599,19 @@ def _maybe_send_alert(
|
|||||||
logger.info(
|
logger.info(
|
||||||
f"trade alert pushed city={city} severity={alert_payload.get('severity')} "
|
f"trade alert pushed city={city} severity={alert_payload.get('severity')} "
|
||||||
f"trigger_count={alert_payload.get('trigger_count')} trigger_key={trigger_key} "
|
f"trigger_count={alert_payload.get('trigger_count')} trigger_key={trigger_key} "
|
||||||
f"evidence={_evidence_brief(alert_payload)}"
|
f"evidence={_evidence_brief(alert_payload)} chat_targets={sent_count}"
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def start_trade_alert_push_loop(bot: Any, config: Dict[str, Any]) -> Optional[threading.Thread]:
|
def start_trade_alert_push_loop(bot: Any, config: Dict[str, Any]) -> Optional[threading.Thread]:
|
||||||
enabled = _env_bool("TELEGRAM_ALERT_PUSH_ENABLED", True)
|
enabled = _env_bool("TELEGRAM_ALERT_PUSH_ENABLED", True)
|
||||||
chat_id = os.getenv("TELEGRAM_CHAT_ID")
|
chat_ids = get_telegram_chat_ids_from_env()
|
||||||
if not enabled:
|
if not enabled:
|
||||||
logger.info("telegram alert push loop disabled")
|
logger.info("telegram alert push loop disabled")
|
||||||
return None
|
return None
|
||||||
if not chat_id:
|
if not chat_ids:
|
||||||
logger.warning("telegram alert push loop skipped: TELEGRAM_CHAT_ID is not set")
|
logger.warning("telegram alert push loop skipped: TELEGRAM_CHAT_IDS is not set")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
mispricing_only = _env_bool("TELEGRAM_ALERT_MISPRICING_ONLY", True)
|
mispricing_only = _env_bool("TELEGRAM_ALERT_MISPRICING_ONLY", True)
|
||||||
@@ -616,7 +634,7 @@ def start_trade_alert_push_loop(bot: Any, config: Dict[str, Any]) -> Optional[th
|
|||||||
logger.exception(f"failed to initialize telegram push state path={state_path}")
|
logger.exception(f"failed to initialize telegram push state path={state_path}")
|
||||||
logger.info(
|
logger.info(
|
||||||
f"telegram alert push loop started mode={'mispricing-only' if mispricing_only else 'full'} "
|
f"telegram alert push loop started mode={'mispricing-only' if mispricing_only else 'full'} "
|
||||||
f"cities={len(cities)} interval={interval_sec}s "
|
f"cities={len(cities)} interval={interval_sec}s chat_targets={len(chat_ids)} "
|
||||||
f"cooldown={cooldown_sec}s min_triggers={min_trigger_count} min_severity={min_severity} "
|
f"cooldown={cooldown_sec}s min_triggers={min_trigger_count} min_severity={min_severity} "
|
||||||
f"state_path={state_path}"
|
f"state_path={state_path}"
|
||||||
)
|
)
|
||||||
@@ -630,7 +648,7 @@ def start_trade_alert_push_loop(bot: Any, config: Dict[str, Any]) -> Optional[th
|
|||||||
alert_payload = build_trade_alert_for_city(city, config)
|
alert_payload = build_trade_alert_for_city(city, config)
|
||||||
if _maybe_send_alert(
|
if _maybe_send_alert(
|
||||||
bot=bot,
|
bot=bot,
|
||||||
chat_id=chat_id,
|
chat_ids=chat_ids,
|
||||||
city=city,
|
city=city,
|
||||||
alert_payload=alert_payload,
|
alert_payload=alert_payload,
|
||||||
state=state,
|
state=state,
|
||||||
|
|||||||
Reference in New Issue
Block a user