diff --git a/.env b/.env deleted file mode 100644 index c4c8c05..0000000 --- a/.env +++ /dev/null @@ -1,2 +0,0 @@ -CM_IMAGE_PREFIX=gitea.04080616.xyz/yiekheng -DOCKER_IMAGE_TAG=latest diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..747d4e2 --- /dev/null +++ b/.env.example @@ -0,0 +1,31 @@ +# === Deployment Identity === +# Unique name prefix for containers and network (avoid conflicts on same host) +CM_DEPLOY_NAME=rex-cm +# Host port for web view (each deployment needs a unique port) +CM_WEB_HOST_PORT=8001 + +# === Docker Registry === +CM_IMAGE_PREFIX=gitea.04080616.xyz/yiekheng +DOCKER_IMAGE_TAG=latest + +# === Telegram === +TELEGRAM_BOT_TOKEN= +TELEGRAM_ALERT_CHAT_ID= +# TELEGRAM_ALERT_BOT_TOKEN= + +# === Database === +DB_HOST= +DB_USER= +DB_PASSWORD= +DB_NAME= +DB_PORT=3306 +DB_CONNECTION_TIMEOUT=8 +DB_CONNECT_RETRIES=5 +DB_CONNECT_RETRY_DELAY=2 + +# === Bot Config === +CM_PREFIX_PATTERN= +CM_AGENT_ID= +CM_AGENT_PASSWORD= +CM_SECURITY_PIN= +CM_BOT_BASE_URL= diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..46f212d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,104 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `app/` contains service modules: + - `cm_api.py` (Flask API, serves on `3000`) + - `cm_web_view.py` (Flask UI, container `8000`, host `8001`) + - `cm_telegram.py` (Telegram bot + account monitor thread) + - `cm_transfer_credit.py` (scheduled transfer worker) + - `db.py` (MySQL connection/retry logic) +- `docker//Dockerfile` builds one image per service (`cm-api`, `cm-web`, `cm-telegram`, `cm-transfer`). +- `docker-compose.yml` uses registry images; `docker-compose.override.yml` swaps to local builds. +- `scripts/local_build.sh` starts local compose; `scripts/publish.sh` builds and pushes all images via buildx. + +## Reproduce From Scratch (Clean Machine) +1. Install prerequisites: + - Docker Engine + Docker Compose v2 + - MySQL 8+ reachable by containers + - Telegram bot token(s) for bot and optional alerting +2. Clone and enter repo: + ```bash + git clone cm_bot_v2 + cd cm_bot_v2 + ``` +3. Create `.env` at repo root for compose interpolation: + ```bash + CM_IMAGE_PREFIX=local + DOCKER_IMAGE_TAG=dev + TELEGRAM_BOT_TOKEN= + TELEGRAM_ALERT_CHAT_ID= + TELEGRAM_ALERT_BOT_TOKEN= + CM_TRANSFER_MAX_THREADS=1 + ``` +4. Prepare MySQL schema (minimum required): + ```sql + CREATE DATABASE rex_cm CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + USE rex_cm; + + CREATE TABLE acc ( + username VARCHAR(64) PRIMARY KEY, + password VARCHAR(128) NOT NULL, + status VARCHAR(32) DEFAULT '', + link VARCHAR(512) DEFAULT '' + ); + + CREATE TABLE user ( + f_username VARCHAR(64) PRIMARY KEY, + f_password VARCHAR(128) NOT NULL, + t_username VARCHAR(64) NOT NULL, + t_password VARCHAR(128) NOT NULL, + last_update_time TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ); + ``` +5. Seed at least one `acc.username` matching prefix `13c...` (required by `CM_BOT_HAL.get_next_username()`), for example: + ```sql + INSERT INTO acc (username, password, status, link) VALUES ('13c1000', 'seed', '', ''); + ``` +6. Configure DB connection values: + - Default fallback is hardcoded in `app/db.py` (`DB_HOST=192.168.0.210`, etc.). + - For reliable reproduction, add `DB_HOST`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`, `DB_PORT` to service `environment:` in compose files (at minimum `api-server`, `telegram-bot`, `transfer-bot`). +7. Start services locally: + ```bash + docker compose -f docker-compose.yml -f docker-compose.override.yml up --build + ``` + Or run `bash scripts/local_build.sh` (uses `sudo` by default). + +## Build, Test, and Development Commands +- `python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt`: optional non-Docker local env. +- `docker compose -f docker-compose.yml -f docker-compose.override.yml up --build`: local dev stack. +- `docker compose up -d`: run prebuilt/published images. +- `bash scripts/publish.sh `: build + push all service images (`gitea.04080616.xyz/yiekheng`). + +## Verification Checklist +- API responds: `curl http://localhost:3000/acc/` +- Web UI loads: open `http://localhost:8001` +- Service logs are clean: + ```bash + docker compose logs -f api-server web-view telegram-bot transfer-bot + ``` +- Telegram bot validates with `/menu` and `/9` in chat after startup. + +## Coding Style & Naming Conventions +- Python 3.9, 4-space indentation, snake_case for variables/functions, module names as `cm_.py`. +- Preserve existing class names (`CM_API`, `CM_BOT`, `CM_BOT_HAL`). +- Keep environment variable names uppercase and document new ones in this file. +- No enforced formatter/linter in-repo; match the surrounding style in touched files. + +## Testing Guidelines +- No automated test suite is currently committed. +- Required minimum before PR: run verification checklist above on local compose. +- For logic-heavy changes, add `pytest` tests under `tests/` and include execution command/results in PR. + +## Commit & Pull Request Guidelines +- Use short, focused commit subjects in imperative tone (existing history: `Fix ...`, `Update ...`, `Refactor ...`). +- Keep each commit scoped to one behavior change. +- PR must include: + - problem statement and solution summary, + - services/files affected, + - required env/config changes, + - API/log evidence (and UI screenshot if `cm_web_view.py` changed). + +## Security & Configuration Tips +- Never commit real secrets in `.env`. +- `app/cm_bot_hal.py` currently contains hardcoded agent credentials/pin; move these to env vars before production use. +- Keep container clocks mounted (`/etc/timezone`, `/etc/localtime`) as compose currently defines to avoid schedule drift. diff --git a/README.md b/README.md new file mode 100644 index 0000000..94dd49a --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# CM Bot v2 – Portainer Setup (Gitea Registry) + +Brief, copy/paste-ready steps to run the published images from `gitea.04080616.xyz` using Portainer. + +## What gets deployed +- `cm-api` (port 3000), `cm-web` (port 8000 → host `CM_WEB_HOST_PORT`), `cm-telegram`, `cm-transfer` +- Container names prefixed with `CM_DEPLOY_NAME` (e.g. `rex-cm-telegram-bot`) +- Docker network: `${CM_DEPLOY_NAME}-network` (bridge) + +## Environment configs + +Pre-configured `.env` files for each deployment are in the `envs/` folder: +``` +envs/ +├── rex/.env # Rex deployment (port 8001) +└── siong/.env # Siong deployment (port 8005) +``` + +For local development, copy the desired env to the project root: +```bash +cp envs/rex/.env .env +# or +cp envs/siong/.env .env +``` + +For Portainer, load the env vars from the appropriate file into the stack environment variables. + +## Key variables +| Variable | Description | +|---|---| +| `CM_DEPLOY_NAME` | Unique prefix for containers/network (e.g. `rex-cm`, `siong-cm`) | +| `CM_WEB_HOST_PORT` | Host port for web view (must be unique per deployment) | +| `TELEGRAM_BOT_TOKEN` | Your Telegram bot token | +| `DB_HOST` / `DB_USER` / `DB_PASSWORD` / `DB_NAME` | Database connection | +| `CM_PREFIX_PATTERN` | Username prefix pattern | +| `CM_AGENT_ID` / `CM_AGENT_PASSWORD` / `CM_SECURITY_PIN` | Agent credentials | +| `CM_BOT_BASE_URL` | Bot API base URL | + +## One-time: add the registry in Portainer +1) Portainer → **Registries** → **Add registry** → **Custom**. +2) Name: `gitea-prod` (any) +3) Registry URL: `gitea.04080616.xyz` +4) Username: your Gitea username; Password: the PAT. Save. + +## Deploy the stack (fast path) +1) Portainer → **Stacks** → **Add stack** → **Web editor**. +2) Paste the contents of `docker-compose.yml` from this repo (not the override). +3) Load all variables from the appropriate `envs//.env` into the stack environment variables. +4) Click **Deploy the stack**. Portainer will pull `cm-:` from `gitea.04080616.xyz/yiekheng` and start all four containers. + +## Updating to a new image tag +1) Edit the stack → change `DOCKER_IMAGE_TAG` → **Update the stack**. +2) Portainer re-pulls and recreates the services with the new tag. + +## Running multiple deployments on same host +Each deployment needs unique values for: +- `CM_DEPLOY_NAME` – avoids container/network name conflicts +- `CM_WEB_HOST_PORT` – avoids port conflicts + +## Common issues +- **Pull denied**: PAT missing `read:package` or wrong username/PAT in the registry entry. +- **Port already allocated**: check `CM_WEB_HOST_PORT` is unique across deployments. +- **No port bindings applied**: ensure network driver stays `bridge` (not `host` or `macvlan`). diff --git a/app/cm_bot.py b/app/cm_bot.py index 365af82..ee315a5 100644 --- a/app/cm_bot.py +++ b/app/cm_bot.py @@ -1,6 +1,7 @@ import datetime import requests, re from bs4 import BeautifulSoup +import os # with open('security_response.html', 'wb') as f: # f.write(response.content) @@ -8,9 +9,15 @@ from bs4 import BeautifulSoup class CM_BOT: def __init__(self): self.session = requests.Session() - self.base_url = 'https://cm99.net' + self.base_url = self._get_required_env('CM_BOT_BASE_URL') self.is_logged_in = False self._setup_headers() + + def _get_required_env(self, name: str) -> str: + value = os.getenv(name) + if value is None or value == "": + raise RuntimeError(f"Missing required environment variable: {name}") + return value def _setup_headers(self): """Set up default headers for requests.""" @@ -323,7 +330,9 @@ class CM_BOT: self.is_logged_in = False return False - def get_max_user_in_pattern(self, prefix_pattern: str = '13c'): + def get_max_user_in_pattern(self, prefix_pattern: str = None): + if prefix_pattern is None: + prefix_pattern = self._get_required_env("CM_PREFIX_PATTERN") response = self.session.get(f'{self.base_url}/cm/json/generateUserTree?id=0') regex = f'\\[{prefix_pattern}\\d+]' matches = re.findall(regex, response.text) @@ -363,12 +372,14 @@ class CM_BOT: if re.search('User created successfully', response.text): print(f"User account: {user_id} password: {user_password} creation completed!") + return True else: print(f"User account: {user_id} creation FAIL!") + return False except requests.exceptions.RequestException as e: print(f"Error creating user account: {e}") - return None + return False # def change_user_password(self, user_id: str, new_user_pass: str): # try: @@ -462,46 +473,7 @@ class CM_BOT: def main(): - # user_id='testing0001' - # user_pass='Qwer1@34' - - # # prefix = '13c' - user_manager = CM_BOT() - - user_manager.login( - username = '13c4021', - password = 'vX34wUk' - ) - - user_manager.transfer_credit('m94', 'Sky533535', 0.01) - - # last_username = user_manager.get_max_user_in_pattern(prefix) - # user_id = f'{prefix}{user_manager.get_generate_username(last_username)}' - # user_pass = user_manager.get_random_password() - - # print(user_id) - # print(user_pass) - - # user_manager.register_user( - # user_id=user_id, - # user_password=user_pass - # ) - - # user_manager.logout() - - # user_manager = CM_BOT( - # username = user_id, - # password = user_pass - # ) - # user_manager.login(user_id, user_pass) - # print(user_manager.get_register_link()) - - # user_manager.login( - # username = user_id, - # password = user_pass - # ) - - # user_manager.set_security_pin(user_pass) + print("CM_BOT helper module. Use from service entrypoints instead of running direct debug actions.") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/app/cm_bot_hal.py b/app/cm_bot_hal.py index e5833c7..4c9ff0a 100644 --- a/app/cm_bot_hal.py +++ b/app/cm_bot_hal.py @@ -4,17 +4,25 @@ from .cm_bot import CM_BOT from .db import DB import secrets, string +import os + + +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 class CM_BOT_HAL: def __init__(self): self.db = DB() - self.prefix = '13c' - self.agent_id = 'cm13a3' - self.agent_password = 'Sky533535' - self.security_pin = 'Sky533535' + self.prefix = _get_required_env('CM_PREFIX_PATTERN') + self.agent_id = _get_required_env('CM_AGENT_ID') + self.agent_password = _get_required_env('CM_AGENT_PASSWORD') + self.security_pin = _get_required_env('CM_SECURITY_PIN') def get_random_password(self): - length = 8 + length = secrets.choice(range(8, 11)) lower_case = string.ascii_lowercase upper_case = string.ascii_uppercase digits = string.digits @@ -77,11 +85,15 @@ class CM_BOT_HAL: password = self.agent_password ) == False: raise Exception(f'[Fail login] {self.agent_id} cannot login.') - cm_bot.register_user( - user_id = username, - user_password = password - ) - cm_bot.logout() + + try: + if cm_bot.register_user( + user_id = username, + user_password = password + ) is False: + raise Exception(f'[Fail create] {username} creation failed.') + finally: + cm_bot.logout() cm_bot = CM_BOT() if cm_bot.login( @@ -197,10 +209,10 @@ class CM_BOT_HAL: if __name__ == '__main__': bot = CM_BOT_HAL() - # bot.transfer_credit_api('13c4070', 'zU2QoL', '4753kit', 'Sky533535') - # print(bot.get_next_username('13c')) + # bot.transfer_credit_api('', '', '', '') + # print(bot.get_next_username(bot.prefix)) # print(bot.get_random_password()) # bot.get_user_api() # bot.insert_user_to_table_acc({'username': 'test0001', 'password': 'test0001', 'link': 'test0001'}) # print(bot.get_whatsapp_link_username('https://chat.whatsapp.com/DZDWcicr6MTFrR4kBfnJXO')) - # print(bot.get_user_pass_from_acc('13c4151')) + # print(bot.get_user_pass_from_acc('')) diff --git a/app/cm_telegram.py b/app/cm_telegram.py index 071583e..a2de796 100644 --- a/app/cm_telegram.py +++ b/app/cm_telegram.py @@ -1,9 +1,13 @@ -import threading, logging, time, asyncio +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 @@ -14,12 +18,36 @@ 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 - Set Security Pin', - '/3 ' + '/3 ', + '/9 - Show Chat ID' ] await update.message.reply_text('\n'.join(menu)) @@ -48,12 +76,12 @@ async def set_security_handler(update: Update, context: ContextTypes.DEFAULT_TYP if len(context.args) == 0 or len(context.args) > 1: await update.message.reply_text('CMD is wrong, please check and retry ...') return - 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 ...') 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']} !") @@ -61,50 +89,175 @@ async def set_security_handler(update: Update, context: ContextTypes.DEFAULT_TYP 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) != 4: + if len(context.args) == 0 or len(context.args) != 2: await update.message.reply_text('CMD is wrong, please check and retry ...') return - bot = CM_BOT_HAL() - f_username, f_password, t_username, t_password = context.args - bot.insert_user_to_table_user( + 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': t_password + 't_password': security_pin } ) - await update.message.reply_text(f'Done insert {f_username} into user table.') + if success is False: + raise Exception('Failed to insert user into table') -def monitor_amount_of_available_acc(): + 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 = CM_BOT_HAL() - available_size = len(bot.get_all_available_acc()) + bot = None + try: + bot = CM_BOT_HAL() + available_size = len(bot.get_all_available_acc()) - if available_size <= max_available: - global creating_acc_now - creating_acc_now = True - for i in range(available_size, max_available): - bot.create_new_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 - time.sleep(10 * 60) - del bot + if bot is not None: + del bot + time.sleep(10 * 60) -def main() -> None: - """Start the bot.""" - # application = Application.builder().token("5327571437:AAFlowwnAysTEMx6LtYQNTevGCboKDZoYzY").build() - application = Application.builder().token("5315819168:AAH31xwNgPdnk123x97XalmTW6fQV5EUCFU").build() +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...") - threading.Thread(target=monitor_amount_of_available_acc, args=()).start() - application.run_polling(allowed_updates=Update.ALL_TYPES) + notifier = TelegramNotifier() + threading.Thread(target=monitor_amount_of_available_acc, args=(notifier,), daemon=True).start() + run_polling_forever(bot_token) if __name__ == "__main__": main() diff --git a/app/cm_web_view.py b/app/cm_web_view.py index 4d925c1..9020b78 100644 --- a/app/cm_web_view.py +++ b/app/cm_web_view.py @@ -9,7 +9,9 @@ CORS(app) # API base URL - use environment variable for Docker Compose import os API_BASE_URL = os.getenv('API_BASE_URL', 'http://localhost:3000') +PREFIX_PATTERN = os.getenv('CM_PREFIX_PATTERN', '') print("API: ", API_BASE_URL) +print("Prefix pattern: ", PREFIX_PATTERN) # Beautiful HTML template with modern styling HTML_TEMPLATE = """ @@ -379,6 +381,7 @@ HTML_TEMPLATE = """