- Remove all hardcoded credentials and config from Python source code:
- db.py: DB host/user/password/name/port → env vars with connection retry support
- cm_bot_hal.py: prefix, agent_id, agent_password, security_pin → env vars
- cm_bot.py: base_url → env var, fix register_user return values
- cm_web_view.py: hardcoded '13c' prefix → configurable CM_PREFIX_PATTERN
- cm_telegram.py: hardcoded 'Sky533535' pin → env var CM_SECURITY_PIN
- Parameterize docker-compose.yml for multi-deployment on same host:
- Container names use ${CM_DEPLOY_NAME} prefix (e.g. rex-cm-*, siong-cm-*)
- Network name uses ${CM_DEPLOY_NAME}-network
- Web view port configurable via ${CM_WEB_HOST_PORT}
- All service config passed as env vars (not baked into image)
- Add per-deployment env configs:
- envs/rex/.env (port 8001, prefix 13c, DB rex_cm)
- envs/siong/.env (port 8005, prefix 13sa, DB siong_cm)
- .env.example as template for new deployments
- Remove .env from .gitignore (local server, safe to commit)
- Improve telegram bot reliability:
- Add retry logic for polling with exponential backoff
- Add error handlers for Conflict, RetryAfter, NetworkError, TimedOut
- Add /9 command to show chat ID
- Add telegram_notifier.py for alert notifications
- Fix error handling in /2 and /3 command handlers
- Fix db.py cursor cleanup (close cursor before connection in finally blocks)
- Fix docker-compose.override.yml environment syntax (list → mapping)
- Update README with multi-deployment instructions
- Add AGENTS.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
264 lines
10 KiB
Python
264 lines
10 KiB
Python
import threading, logging, time, asyncio, os
|
|
from typing import Optional
|
|
|
|
from telegram import ForceReply, Update
|
|
from telegram.error import Conflict, InvalidToken, NetworkError, RetryAfter, TimedOut
|
|
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
|
|
from telegram.request import HTTPXRequest
|
|
|
|
from .cm_bot_hal import CM_BOT_HAL
|
|
from .telegram_notifier import TelegramNotifier, get_telegram_bot_token
|
|
|
|
logging.basicConfig(
|
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
|
|
)
|
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
creating_acc_now = False
|
|
|
|
def _get_required_env(name: str) -> str:
|
|
value = os.getenv(name)
|
|
if value is None or value == "":
|
|
raise RuntimeError(f"Missing required environment variable: {name}")
|
|
return value
|
|
|
|
def _get_env_float(name: str, default: float) -> float:
|
|
try:
|
|
return float(os.getenv(name, str(default)))
|
|
except ValueError:
|
|
return default
|
|
|
|
def _get_env_int(name: str, default: int) -> int:
|
|
try:
|
|
return int(os.getenv(name, str(default)))
|
|
except ValueError:
|
|
return default
|
|
|
|
def _retry_after_seconds(retry_after) -> int:
|
|
if hasattr(retry_after, "total_seconds"):
|
|
return max(1, int(retry_after.total_seconds()))
|
|
return max(1, int(retry_after))
|
|
|
|
async def menu_cmd_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
menu = [
|
|
'MENU',
|
|
'/1 - Get Acc',
|
|
'/2 <link> - Set Security Pin',
|
|
'/3 <agent username> <player username>',
|
|
'/9 - Show Chat ID'
|
|
]
|
|
await update.message.reply_text('\n'.join(menu))
|
|
|
|
async def get_acc_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
global creating_acc_now
|
|
while creating_acc_now == True:
|
|
await update.message.reply_text('CM account creation is running, queuing ...')
|
|
await asyncio.sleep(1)
|
|
creating_acc_now = True
|
|
await update.message.reply_text('Start Getting CM Account ...')
|
|
try:
|
|
bot = CM_BOT_HAL()
|
|
user = bot.get_user_api()
|
|
msg = [
|
|
f'Username: {user["username"]}',
|
|
f'Password: {user["password"]}',
|
|
f'Link: {user["link"]}'
|
|
]
|
|
await update.message.reply_text('\n'.join(msg))
|
|
except Exception as e:
|
|
await update.message.reply_text(f'Error: {e}')
|
|
finally:
|
|
creating_acc_now = False
|
|
|
|
async def set_security_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
if len(context.args) == 0 or len(context.args) > 1:
|
|
await update.message.reply_text('CMD is wrong, please check and retry ...')
|
|
return
|
|
try:
|
|
bot = CM_BOT_HAL()
|
|
if bot.is_whatsapp_url(context.args[0]) == False:
|
|
await update.message.reply_text('Link Format Wrong, please check and retry ...')
|
|
return
|
|
await update.message.reply_text('Start Setting Security Pin ...')
|
|
result = bot.set_security_pin_api(context.args[0])
|
|
del bot
|
|
await update.message.reply_text(f"Done setting Security Pin for {result['f_username']} - {result['t_username']} !")
|
|
except Exception as e:
|
|
await update.message.reply_text(f'Error: {e}')
|
|
|
|
async def insert_to_user_table_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
if len(context.args) == 0 or len(context.args) != 2:
|
|
await update.message.reply_text('CMD is wrong, please check and retry ...')
|
|
return
|
|
try:
|
|
bot = CM_BOT_HAL()
|
|
f_username, t_username = context.args
|
|
security_pin = _get_required_env("CM_SECURITY_PIN")
|
|
f_password = bot.get_user_pass_from_acc(f_username)
|
|
if not f_password:
|
|
raise Exception(f'Cannot find password for {f_username}')
|
|
|
|
success = bot.insert_user_to_table_user(
|
|
{
|
|
'f_username': f_username,
|
|
'f_password': f_password,
|
|
't_username': t_username,
|
|
't_password': security_pin
|
|
}
|
|
)
|
|
if success is False:
|
|
raise Exception('Failed to insert user into table')
|
|
|
|
await update.message.reply_text(f'Done insert {f_username} into user table.')
|
|
except Exception as e:
|
|
await update.message.reply_text(f'Error: {e}')
|
|
|
|
async def show_chat_id_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
chat_id = update.effective_chat.id if update.effective_chat else "Unknown"
|
|
await update.message.reply_text(f'Chat ID: {chat_id}')
|
|
|
|
def monitor_amount_of_available_acc(notifier: Optional[TelegramNotifier] = None):
|
|
global creating_acc_now
|
|
max_available = 20
|
|
notifier = notifier or TelegramNotifier()
|
|
|
|
while True:
|
|
bot = None
|
|
try:
|
|
bot = CM_BOT_HAL()
|
|
available_size = len(bot.get_all_available_acc())
|
|
|
|
if available_size <= max_available:
|
|
creating_acc_now = True
|
|
for i in range(available_size, max_available):
|
|
try:
|
|
bot.create_new_acc()
|
|
except Exception as exc:
|
|
err_text = str(exc)
|
|
logger.exception("Failed to auto create CM account: %s", err_text)
|
|
if notifier:
|
|
if '[Fail login]' in err_text:
|
|
notifier.notify_login_failure(err_text)
|
|
else:
|
|
notifier.notify_generic_error(err_text)
|
|
break
|
|
except Exception as exc:
|
|
logger.exception("Unexpected error while monitoring accounts: %s", exc)
|
|
if notifier:
|
|
notifier.notify_generic_error(str(exc))
|
|
finally:
|
|
creating_acc_now = False
|
|
if bot is not None:
|
|
del bot
|
|
time.sleep(10 * 60)
|
|
|
|
async def telegram_error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
err = context.error
|
|
if err is None:
|
|
return
|
|
|
|
if isinstance(err, Conflict):
|
|
logger.warning(
|
|
"Telegram polling conflict detected. Ensure only one bot instance runs with this token. %s",
|
|
err,
|
|
)
|
|
return
|
|
|
|
if isinstance(err, RetryAfter):
|
|
wait_seconds = _retry_after_seconds(err.retry_after)
|
|
logger.warning("Telegram flood control exceeded. Retry after %s seconds.", wait_seconds)
|
|
return
|
|
|
|
if isinstance(err, (NetworkError, TimedOut)):
|
|
logger.warning("Telegram network error: %s", err)
|
|
return
|
|
|
|
logger.exception("Unhandled Telegram update error", exc_info=err)
|
|
|
|
def build_application(bot_token: str) -> Application:
|
|
request_kwargs = dict(
|
|
connect_timeout=_get_env_float('TELEGRAM_CONNECT_TIMEOUT', 10.0),
|
|
read_timeout=_get_env_float('TELEGRAM_READ_TIMEOUT', 30.0),
|
|
write_timeout=_get_env_float('TELEGRAM_WRITE_TIMEOUT', 30.0),
|
|
pool_timeout=_get_env_float('TELEGRAM_POOL_TIMEOUT', 10.0),
|
|
)
|
|
request = HTTPXRequest(**request_kwargs)
|
|
get_updates_request = HTTPXRequest(**request_kwargs)
|
|
application = (
|
|
Application.builder()
|
|
.token(bot_token)
|
|
.request(request)
|
|
.get_updates_request(get_updates_request)
|
|
.build()
|
|
)
|
|
application.add_handler(CommandHandler("menu", menu_cmd_handler))
|
|
application.add_handler(CommandHandler("1", get_acc_handler))
|
|
application.add_handler(CommandHandler("2", set_security_handler))
|
|
application.add_handler(CommandHandler("3", insert_to_user_table_handler))
|
|
application.add_handler(CommandHandler("9", show_chat_id_handler))
|
|
application.add_error_handler(telegram_error_handler)
|
|
# application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
|
|
return application
|
|
|
|
def run_polling_forever(bot_token: str) -> None:
|
|
retry_delay = max(1, _get_env_int('TELEGRAM_POLLING_RETRY_DELAY', 5))
|
|
max_retry_delay = max(retry_delay, _get_env_int('TELEGRAM_POLLING_MAX_RETRY_DELAY', 60))
|
|
bootstrap_retries = _get_env_int('TELEGRAM_BOOTSTRAP_RETRIES', 0)
|
|
|
|
while True:
|
|
logger.info(
|
|
"Starting Telegram polling (bootstrap_retries=%s, long_poll_timeout=%s, retry_delay=%ss).",
|
|
bootstrap_retries,
|
|
_get_env_int('TELEGRAM_LONG_POLL_TIMEOUT', 30),
|
|
retry_delay,
|
|
)
|
|
application = build_application(bot_token)
|
|
try:
|
|
application.run_polling(
|
|
allowed_updates=Update.ALL_TYPES,
|
|
timeout=_get_env_int('TELEGRAM_LONG_POLL_TIMEOUT', 30),
|
|
bootstrap_retries=bootstrap_retries,
|
|
)
|
|
return
|
|
except InvalidToken as exc:
|
|
logger.exception(
|
|
"Invalid Telegram bot token. Check TELEGRAM_BOT_TOKEN and restart after fixing it: %s",
|
|
exc,
|
|
)
|
|
raise
|
|
except RetryAfter as exc:
|
|
wait_seconds = _retry_after_seconds(exc.retry_after)
|
|
logger.warning("Telegram flood control exceeded. Restarting polling in %s seconds.", wait_seconds)
|
|
time.sleep(wait_seconds)
|
|
retry_delay = min(wait_seconds, max_retry_delay)
|
|
except Conflict as exc:
|
|
logger.warning("Telegram polling conflict detected: %s", exc)
|
|
logger.warning("Another bot instance is likely running with the same token. Retrying in %s seconds.", retry_delay)
|
|
time.sleep(retry_delay)
|
|
retry_delay = min(retry_delay * 2, max_retry_delay)
|
|
except (NetworkError, TimedOut) as exc:
|
|
logger.warning("Telegram network error while polling: %s", exc)
|
|
logger.warning("Retrying polling in %s seconds.", retry_delay)
|
|
time.sleep(retry_delay)
|
|
retry_delay = min(retry_delay * 2, max_retry_delay)
|
|
except Exception as exc:
|
|
logger.exception("Unexpected polling crash: %s", exc)
|
|
logger.warning("Retrying polling in %s seconds.", retry_delay)
|
|
time.sleep(retry_delay)
|
|
retry_delay = min(retry_delay * 2, max_retry_delay)
|
|
|
|
def main() -> None:
|
|
"""Start the bot."""
|
|
bot_token = get_telegram_bot_token()
|
|
|
|
# Start the Telegram bot
|
|
print("Starting Telegram bot...")
|
|
notifier = TelegramNotifier()
|
|
threading.Thread(target=monitor_amount_of_available_acc, args=(notifier,), daemon=True).start()
|
|
run_polling_forever(bot_token)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|