feat: add telegram group pricing and direct payments

This commit is contained in:
2569718930@qq.com
2026-05-18 16:18:26 +08:00
parent d99a3c25e9
commit 6041d25f23
18 changed files with 1479 additions and 35 deletions
+147
View File
@@ -0,0 +1,147 @@
from __future__ import annotations
import hashlib
import hmac
import os
import time
from decimal import Decimal
from typing import Any, Dict, Optional
import requests
from src.utils.telegram_chat_ids import parse_telegram_chat_ids
TELEGRAM_MEMBER_STATUSES = {"creator", "administrator", "member"}
def _format_decimal(value: Decimal) -> str:
text = format(value.normalize(), "f")
if "." in text:
text = text.rstrip("0").rstrip(".")
return text or "0"
def _decimal_env(name: str, default: str) -> Decimal:
raw = str(os.getenv(name) or default).strip()
try:
return Decimal(raw)
except Exception:
return Decimal(default)
def _telegram_data_check_string(payload: Dict[str, Any]) -> str:
items = []
for key in sorted(payload.keys()):
if key == "hash":
continue
value = payload.get(key)
if value is None:
continue
items.append(f"{key}={value}")
return "\n".join(items)
def verify_telegram_login_payload(
payload: Dict[str, Any],
bot_token: str,
*,
max_age_sec: int = 86400,
) -> Dict[str, Any]:
token = str(bot_token or "").strip()
if not token:
raise ValueError("telegram bot token missing")
expected_hash = str(payload.get("hash") or "").strip().lower()
if not expected_hash:
raise ValueError("telegram login hash missing")
data_check = _telegram_data_check_string(payload)
secret = hashlib.sha256(token.encode("utf-8")).digest()
actual_hash = hmac.new(
secret,
data_check.encode("utf-8"),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(actual_hash, expected_hash):
raise ValueError("invalid telegram login hash")
try:
auth_date = int(payload.get("auth_date") or 0)
except Exception as exc:
raise ValueError("invalid telegram auth_date") from exc
if auth_date <= 0:
raise ValueError("invalid telegram auth_date")
if max_age_sec > 0 and int(time.time()) - auth_date > max_age_sec:
raise ValueError("telegram login payload expired")
try:
telegram_id = int(payload.get("id") or 0)
except Exception as exc:
raise ValueError("invalid telegram id") from exc
if telegram_id <= 0:
raise ValueError("invalid telegram id")
return {
"telegram_id": telegram_id,
"username": str(payload.get("username") or "").strip(),
"first_name": str(payload.get("first_name") or "").strip(),
"last_name": str(payload.get("last_name") or "").strip(),
"photo_url": str(payload.get("photo_url") or "").strip(),
"auth_date": auth_date,
}
class TelegramGroupPricing:
def __init__(self) -> None:
self.bot_token = str(os.getenv("TELEGRAM_BOT_TOKEN") or "").strip()
self.group_chat_ids = parse_telegram_chat_ids(
os.getenv("POLYWEATHER_TELEGRAM_GROUP_ID"),
os.getenv("POLYWEATHER_TELEGRAM_GROUP_IDS"),
os.getenv("TELEGRAM_CHAT_IDS"),
os.getenv("TELEGRAM_CHAT_ID"),
)
self.member_price = _decimal_env("POLYWEATHER_GROUP_MEMBER_PRICE_USDC", "5")
self.public_price = _decimal_env("POLYWEATHER_PUBLIC_PRICE_USDC", "10")
self.timeout_sec = max(
2,
int(str(os.getenv("POLYWEATHER_TELEGRAM_HTTP_TIMEOUT_SEC") or "8").strip() or "8"),
)
@property
def configured(self) -> bool:
return bool(self.bot_token and self.group_chat_ids)
def verify_login_payload(self, payload: Dict[str, Any]) -> Dict[str, Any]:
max_age = int(str(os.getenv("POLYWEATHER_TELEGRAM_LOGIN_MAX_AGE_SEC") or "86400"))
return verify_telegram_login_payload(payload, self.bot_token, max_age_sec=max_age)
def get_member_status(self, telegram_id: int) -> Optional[str]:
if not self.configured:
return None
for chat_id in self.group_chat_ids:
try:
response = requests.get(
f"https://api.telegram.org/bot{self.bot_token}/getChatMember",
params={"chat_id": chat_id, "user_id": int(telegram_id)},
timeout=self.timeout_sec,
)
data = response.json()
except Exception:
continue
if not isinstance(data, dict) or not data.get("ok"):
continue
result = data.get("result")
if not isinstance(result, dict):
continue
status = str(result.get("status") or "").strip().lower()
if status:
return status
return None
def resolve_price_for_telegram_id(self, telegram_id: Optional[int]) -> Dict[str, Any]:
status = self.get_member_status(int(telegram_id or 0)) if telegram_id else None
is_member = bool(status in TELEGRAM_MEMBER_STATUSES)
amount = self.member_price if is_member else self.public_price
return {
"configured": self.configured,
"telegram_id": int(telegram_id or 0) or None,
"telegram_status": status,
"is_group_member": is_member,
"amount_usdc": _format_decimal(amount),
"pricing_source": "telegram_group_member" if is_member else "telegram_public",
}