Files
AutoDoneTeam/modules/billing.py
dela d146ad9ebd feat: 添加完整的 Telegram Bot 和欧洲账单生成功能
主要更新:
-  新增 Telegram Bot 交互界面
-  新增欧洲账单自动生成功能
- 📦 整理项目结构,部署文件移至 deployment/ 目录
- 📝 完善文档,新增 CHANGELOG 和 Bot 部署指南
- 🔧 统一使用 pyproject.toml 管理依赖(支持 uv)
- 🛡️  增强 .gitignore,防止敏感配置泄露

新增文件:
- tg_bot.py: Telegram Bot 主程序
- generate_billing.py: 独立账单生成工具
- modules/billing.py: 欧洲账单生成模块
- deployment/: Docker、systemd 等部署配置
- docs/: 完整的文档和更新日志

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 09:59:13 +08:00

272 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# modules/billing.py
"""EU Billing Generator for ChatGPT Team Plans
This module handles the generation of EU billing checkout URLs for registered accounts.
It requires a valid access token obtained from an authenticated session.
"""
from dataclasses import dataclass
from typing import Optional, Dict, Any
import random
import requests
from config import EU_BILLING_CONFIG, FINGERPRINT_CONFIG, DEBUG
@dataclass(frozen=True)
class BillingResult:
"""Result of billing checkout generation
Attributes:
success: Whether billing generation succeeded
checkout_url: Generated checkout URL (if successful)
error: Error message (if failed)
raw_response: Raw API response data (for debugging)
"""
success: bool
checkout_url: Optional[str] = None
error: Optional[str] = None
raw_response: Optional[Dict[str, Any]] = None
class DeviceIDGenerator:
"""Generate device IDs for billing requests"""
@staticmethod
def generate() -> str:
"""Generate UUID-like device ID
Format: 8-4-4-4-12 hex characters
Example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Returns:
Device ID string
"""
def random_hex(length: int) -> str:
return ''.join(random.choices('0123456789abcdef', k=length))
return f"{random_hex(8)}-{random_hex(4)}-{random_hex(4)}-{random_hex(4)}-{random_hex(12)}"
class EUBillingGenerator:
"""Generate EU billing checkout URLs for ChatGPT Team Plans"""
def __init__(self, user_agent: Optional[str] = None):
"""Initialize billing generator
Args:
user_agent: Custom user agent (defaults to FINGERPRINT_CONFIG if None)
"""
self.user_agent = user_agent or FINGERPRINT_CONFIG.get('user_agent',
'Mozilla/5.0 (X11; Linux x86_64; rv:146.0) Gecko/20100101 Firefox/146.0')
def generate_checkout_url(self, access_token: str, timeout: int = 30) -> BillingResult:
"""Generate EU billing checkout URL
Args:
access_token: Valid ChatGPT access token
timeout: Request timeout in seconds
Returns:
BillingResult with checkout URL or error
"""
# Generate unique device ID for this request
device_id = DeviceIDGenerator.generate()
# Build request components
headers = self._build_headers(access_token, device_id)
payload = self._build_payload()
url = EU_BILLING_CONFIG.get('checkout_endpoint',
'https://chatgpt.com/backend-api/payments/checkout')
try:
# Send POST request to checkout endpoint
response = requests.post(
url,
headers=headers,
json=payload,
timeout=timeout,
allow_redirects=False
)
# Parse JSON response
try:
result_data = response.json()
except requests.exceptions.JSONDecodeError:
if DEBUG:
print(f"❌ [Billing] Failed to parse JSON response")
print(f" Status Code: {response.status_code}")
print(f" Response Text: {response.text[:500]}...")
print(f" Response Headers: {dict(response.headers)}")
return BillingResult(
success=False,
error=f"Invalid JSON response (HTTP {response.status_code})",
raw_response={'status_code': response.status_code, 'text': response.text[:500]}
)
# Handle successful response
if response.status_code == 200:
checkout_url = self._construct_checkout_url(result_data)
if checkout_url:
if DEBUG:
print(f"✅ [Billing] Checkout URL generated")
print(f" URL: {checkout_url}")
return BillingResult(
success=True,
checkout_url=checkout_url,
raw_response=result_data
)
else:
error_msg = f"No checkout URL in response: {result_data}"
if DEBUG:
print(f"❌ [Billing] {error_msg}")
return BillingResult(
success=False,
error=error_msg,
raw_response=result_data
)
# Handle error responses
else:
detail = result_data.get('detail', result_data)
error_msg = f"HTTP {response.status_code}: {detail}"
if DEBUG:
print(f"❌ [Billing] API error: {error_msg}")
return BillingResult(
success=False,
error=error_msg,
raw_response=result_data
)
except requests.RequestException as e:
error_msg = f"Network error: {str(e)}"
if DEBUG:
print(f"❌ [Billing] {error_msg}")
return BillingResult(
success=False,
error=error_msg
)
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
if DEBUG:
print(f"❌ [Billing] {error_msg}")
return BillingResult(
success=False,
error=error_msg
)
def _build_headers(self, access_token: str, device_id: str) -> Dict[str, str]:
"""Build request headers with all required fields
Args:
access_token: ChatGPT access token
device_id: Generated device ID
Returns:
Complete headers dictionary
"""
# Get OAI client version from config
oai_version = EU_BILLING_CONFIG.get('oai_client_version',
'prod-04eaaa443c69cfc8b46b5d52d2b61dbceba21862')
oai_build = EU_BILLING_CONFIG.get('oai_client_build_number', '4053703')
headers = {
'User-Agent': self.user_agent,
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate', # 移除 br让 requests 自动处理 gzip
'Referer': 'https://chatgpt.com/',
'OAI-Language': 'en-US',
'OAI-Device-Id': device_id,
'OAI-Client-Version': oai_version,
'OAI-Client-Build-Number': str(oai_build),
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json',
'Origin': 'https://chatgpt.com',
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Priority': 'u=4',
'TE': 'trailers'
}
return headers
def _build_payload(self) -> Dict[str, Any]:
"""Build checkout payload from EU_BILLING_CONFIG
Returns:
Complete request payload
"""
payload = {
"plan_name": EU_BILLING_CONFIG.get('plan_name', 'chatgptteamplan'),
"team_plan_data": EU_BILLING_CONFIG.get('team_plan_data', {
'workspace_name': 'Sepa',
'price_interval': 'month',
'seat_quantity': 5,
}),
"billing_details": EU_BILLING_CONFIG.get('billing_details', {
'country': 'DE',
'currency': 'EUR',
}),
"promo_campaign": EU_BILLING_CONFIG.get('promo_campaign', {
'promo_campaign_id': 'team-1-month-free',
'is_coupon_from_query_param': False,
}),
"checkout_ui_mode": EU_BILLING_CONFIG.get('checkout_ui_mode', 'redirect'),
}
return payload
def _construct_checkout_url(self, response_data: Dict[str, Any]) -> Optional[str]:
"""Construct checkout URL from API response
Handles two cases:
1. Direct 'url' field in response
2. Manual construction from 'checkout_session_id' + 'processor_entity'
Args:
response_data: API response JSON
Returns:
Checkout URL or None if construction fails
"""
# Case 1: Direct URL provided
checkout_url = response_data.get('url')
if checkout_url:
return checkout_url
# Case 2: Construct from session ID and processor entity
checkout_session_id = response_data.get('checkout_session_id')
if checkout_session_id:
processor_entity = response_data.get('processor_entity', 'openai_llc')
checkout_url = f"https://chatgpt.com/checkout/{processor_entity}/{checkout_session_id}"
return checkout_url
# No URL could be constructed
return None
# Convenience function for direct usage
def create_eu_billing(access_token: str, timeout: int = 30) -> BillingResult:
"""Convenience function to generate EU billing checkout URL
Args:
access_token: Valid ChatGPT access token
timeout: Request timeout in seconds
Returns:
BillingResult with checkout URL or error
"""
generator = EUBillingGenerator()
return generator.generate_checkout_url(access_token, timeout)