Refactor: externalize all hardcoded config to env vars, add multi-deployment support
- 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>
This commit is contained in:
parent
d73439698a
commit
45303d00aa
31
.env.example
Normal file
31
.env.example
Normal file
@ -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=
|
||||||
104
AGENTS.md
Normal file
104
AGENTS.md
Normal file
@ -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/<service>/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 <repo-url> 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=<required>
|
||||||
|
TELEGRAM_ALERT_CHAT_ID=<optional>
|
||||||
|
TELEGRAM_ALERT_BOT_TOKEN=<optional>
|
||||||
|
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 <tag>`: 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_<role>.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.
|
||||||
63
README.md
Normal file
63
README.md
Normal file
@ -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/<name>/.env` into the stack environment variables.
|
||||||
|
4) Click **Deploy the stack**. Portainer will pull `cm-<service>:<tag>` 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`).
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import requests, re
|
import requests, re
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
import os
|
||||||
|
|
||||||
# with open('security_response.html', 'wb') as f:
|
# with open('security_response.html', 'wb') as f:
|
||||||
# f.write(response.content)
|
# f.write(response.content)
|
||||||
@ -8,9 +9,15 @@ from bs4 import BeautifulSoup
|
|||||||
class CM_BOT:
|
class CM_BOT:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.session = requests.Session()
|
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.is_logged_in = False
|
||||||
self._setup_headers()
|
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):
|
def _setup_headers(self):
|
||||||
"""Set up default headers for requests."""
|
"""Set up default headers for requests."""
|
||||||
@ -323,7 +330,9 @@ class CM_BOT:
|
|||||||
self.is_logged_in = False
|
self.is_logged_in = False
|
||||||
return 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')
|
response = self.session.get(f'{self.base_url}/cm/json/generateUserTree?id=0')
|
||||||
regex = f'\\[{prefix_pattern}\\d+]'
|
regex = f'\\[{prefix_pattern}\\d+]'
|
||||||
matches = re.findall(regex, response.text)
|
matches = re.findall(regex, response.text)
|
||||||
@ -363,12 +372,14 @@ class CM_BOT:
|
|||||||
|
|
||||||
if re.search('User created successfully', response.text):
|
if re.search('User created successfully', response.text):
|
||||||
print(f"User account: {user_id} password: {user_password} creation completed!")
|
print(f"User account: {user_id} password: {user_password} creation completed!")
|
||||||
|
return True
|
||||||
else:
|
else:
|
||||||
print(f"User account: {user_id} creation FAIL!")
|
print(f"User account: {user_id} creation FAIL!")
|
||||||
|
return False
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
print(f"Error creating user account: {e}")
|
print(f"Error creating user account: {e}")
|
||||||
return None
|
return False
|
||||||
|
|
||||||
# def change_user_password(self, user_id: str, new_user_pass: str):
|
# def change_user_password(self, user_id: str, new_user_pass: str):
|
||||||
# try:
|
# try:
|
||||||
@ -462,46 +473,7 @@ class CM_BOT:
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# user_id='testing0001'
|
print("CM_BOT helper module. Use from service entrypoints instead of running direct debug actions.")
|
||||||
# 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)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@ -4,17 +4,25 @@ from .cm_bot import CM_BOT
|
|||||||
from .db import DB
|
from .db import DB
|
||||||
|
|
||||||
import secrets, string
|
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:
|
class CM_BOT_HAL:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.db = DB()
|
self.db = DB()
|
||||||
self.prefix = '13c'
|
self.prefix = _get_required_env('CM_PREFIX_PATTERN')
|
||||||
self.agent_id = 'cm13a3'
|
self.agent_id = _get_required_env('CM_AGENT_ID')
|
||||||
self.agent_password = 'Sky533535'
|
self.agent_password = _get_required_env('CM_AGENT_PASSWORD')
|
||||||
self.security_pin = 'Sky533535'
|
self.security_pin = _get_required_env('CM_SECURITY_PIN')
|
||||||
|
|
||||||
def get_random_password(self):
|
def get_random_password(self):
|
||||||
length = 8
|
length = secrets.choice(range(8, 11))
|
||||||
lower_case = string.ascii_lowercase
|
lower_case = string.ascii_lowercase
|
||||||
upper_case = string.ascii_uppercase
|
upper_case = string.ascii_uppercase
|
||||||
digits = string.digits
|
digits = string.digits
|
||||||
@ -77,11 +85,15 @@ class CM_BOT_HAL:
|
|||||||
password = self.agent_password
|
password = self.agent_password
|
||||||
) == False:
|
) == False:
|
||||||
raise Exception(f'[Fail login] {self.agent_id} cannot login.')
|
raise Exception(f'[Fail login] {self.agent_id} cannot login.')
|
||||||
cm_bot.register_user(
|
|
||||||
user_id = username,
|
try:
|
||||||
user_password = password
|
if cm_bot.register_user(
|
||||||
)
|
user_id = username,
|
||||||
cm_bot.logout()
|
user_password = password
|
||||||
|
) is False:
|
||||||
|
raise Exception(f'[Fail create] {username} creation failed.')
|
||||||
|
finally:
|
||||||
|
cm_bot.logout()
|
||||||
|
|
||||||
cm_bot = CM_BOT()
|
cm_bot = CM_BOT()
|
||||||
if cm_bot.login(
|
if cm_bot.login(
|
||||||
@ -197,10 +209,10 @@ class CM_BOT_HAL:
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
bot = CM_BOT_HAL()
|
bot = CM_BOT_HAL()
|
||||||
# bot.transfer_credit_api('13c4070', 'zU2QoL', '4753kit', 'Sky533535')
|
# bot.transfer_credit_api('<from_user>', '<from_password>', '<to_user>', '<to_pin>')
|
||||||
# print(bot.get_next_username('13c'))
|
# print(bot.get_next_username(bot.prefix))
|
||||||
# print(bot.get_random_password())
|
# print(bot.get_random_password())
|
||||||
# bot.get_user_api()
|
# bot.get_user_api()
|
||||||
# bot.insert_user_to_table_acc({'username': 'test0001', 'password': 'test0001', 'link': 'test0001'})
|
# 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_whatsapp_link_username('https://chat.whatsapp.com/DZDWcicr6MTFrR4kBfnJXO'))
|
||||||
# print(bot.get_user_pass_from_acc('13c4151'))
|
# print(bot.get_user_pass_from_acc('<username>'))
|
||||||
|
|||||||
@ -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 import ForceReply, Update
|
||||||
|
from telegram.error import Conflict, InvalidToken, NetworkError, RetryAfter, TimedOut
|
||||||
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
|
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
|
||||||
|
from telegram.request import HTTPXRequest
|
||||||
|
|
||||||
from .cm_bot_hal import CM_BOT_HAL
|
from .cm_bot_hal import CM_BOT_HAL
|
||||||
|
from .telegram_notifier import TelegramNotifier, get_telegram_bot_token
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
|
||||||
@ -14,12 +18,36 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
creating_acc_now = False
|
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:
|
async def menu_cmd_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
menu = [
|
menu = [
|
||||||
'MENU',
|
'MENU',
|
||||||
'/1 - Get Acc',
|
'/1 - Get Acc',
|
||||||
'/2 <link> - Set Security Pin',
|
'/2 <link> - Set Security Pin',
|
||||||
'/3 <agent username> <agent password> <player username> <player password>'
|
'/3 <agent username> <player username>',
|
||||||
|
'/9 - Show Chat ID'
|
||||||
]
|
]
|
||||||
await update.message.reply_text('\n'.join(menu))
|
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:
|
if len(context.args) == 0 or len(context.args) > 1:
|
||||||
await update.message.reply_text('CMD is wrong, please check and retry ...')
|
await update.message.reply_text('CMD is wrong, please check and retry ...')
|
||||||
return
|
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:
|
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])
|
result = bot.set_security_pin_api(context.args[0])
|
||||||
del bot
|
del bot
|
||||||
await update.message.reply_text(f"Done setting Security Pin for {result['f_username']} - {result['t_username']} !")
|
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}')
|
await update.message.reply_text(f'Error: {e}')
|
||||||
|
|
||||||
async def insert_to_user_table_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
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 ...')
|
await update.message.reply_text('CMD is wrong, please check and retry ...')
|
||||||
return
|
return
|
||||||
bot = CM_BOT_HAL()
|
try:
|
||||||
f_username, f_password, t_username, t_password = context.args
|
bot = CM_BOT_HAL()
|
||||||
bot.insert_user_to_table_user(
|
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_username': f_username,
|
||||||
'f_password': f_password,
|
'f_password': f_password,
|
||||||
't_username': t_username,
|
'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
|
max_available = 20
|
||||||
|
notifier = notifier or TelegramNotifier()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
bot = CM_BOT_HAL()
|
bot = None
|
||||||
available_size = len(bot.get_all_available_acc())
|
try:
|
||||||
|
bot = CM_BOT_HAL()
|
||||||
|
available_size = len(bot.get_all_available_acc())
|
||||||
|
|
||||||
if available_size <= max_available:
|
if available_size <= max_available:
|
||||||
global creating_acc_now
|
creating_acc_now = True
|
||||||
creating_acc_now = True
|
for i in range(available_size, max_available):
|
||||||
for i in range(available_size, max_available):
|
try:
|
||||||
bot.create_new_acc()
|
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
|
creating_acc_now = False
|
||||||
time.sleep(10 * 60)
|
if bot is not None:
|
||||||
del bot
|
del bot
|
||||||
|
time.sleep(10 * 60)
|
||||||
|
|
||||||
def main() -> None:
|
async def telegram_error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Start the bot."""
|
err = context.error
|
||||||
# application = Application.builder().token("5327571437:AAFlowwnAysTEMx6LtYQNTevGCboKDZoYzY").build()
|
if err is None:
|
||||||
application = Application.builder().token("5315819168:AAH31xwNgPdnk123x97XalmTW6fQV5EUCFU").build()
|
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("menu", menu_cmd_handler))
|
||||||
application.add_handler(CommandHandler("1", get_acc_handler))
|
application.add_handler(CommandHandler("1", get_acc_handler))
|
||||||
application.add_handler(CommandHandler("2", set_security_handler))
|
application.add_handler(CommandHandler("2", set_security_handler))
|
||||||
application.add_handler(CommandHandler("3", insert_to_user_table_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))
|
# 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
|
# Start the Telegram bot
|
||||||
print("Starting Telegram bot...")
|
print("Starting Telegram bot...")
|
||||||
threading.Thread(target=monitor_amount_of_available_acc, args=()).start()
|
notifier = TelegramNotifier()
|
||||||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
threading.Thread(target=monitor_amount_of_available_acc, args=(notifier,), daemon=True).start()
|
||||||
|
run_polling_forever(bot_token)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@ -9,7 +9,9 @@ CORS(app)
|
|||||||
# API base URL - use environment variable for Docker Compose
|
# API base URL - use environment variable for Docker Compose
|
||||||
import os
|
import os
|
||||||
API_BASE_URL = os.getenv('API_BASE_URL', 'http://localhost:3000')
|
API_BASE_URL = os.getenv('API_BASE_URL', 'http://localhost:3000')
|
||||||
|
PREFIX_PATTERN = os.getenv('CM_PREFIX_PATTERN', '')
|
||||||
print("API: ", API_BASE_URL)
|
print("API: ", API_BASE_URL)
|
||||||
|
print("Prefix pattern: ", PREFIX_PATTERN)
|
||||||
|
|
||||||
# Beautiful HTML template with modern styling
|
# Beautiful HTML template with modern styling
|
||||||
HTML_TEMPLATE = """
|
HTML_TEMPLATE = """
|
||||||
@ -379,6 +381,7 @@ HTML_TEMPLATE = """
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const PREFIX_PATTERN = {{ prefix_pattern|tojson }};
|
||||||
let currentTab = 'acc';
|
let currentTab = 'acc';
|
||||||
let accData = [];
|
let accData = [];
|
||||||
let userData = [];
|
let userData = [];
|
||||||
@ -440,7 +443,7 @@ HTML_TEMPLATE = """
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort data with 13c prefix priority
|
// Sort data with configured prefix priority
|
||||||
const sortedData = sortData([...accData], 'acc');
|
const sortedData = sortData([...accData], 'acc');
|
||||||
|
|
||||||
const table = `
|
const table = `
|
||||||
@ -487,7 +490,7 @@ HTML_TEMPLATE = """
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort data with 13c prefix priority and by update time
|
// Sort data with configured prefix priority and by update time
|
||||||
const sortedData = sortData([...userData], 'user');
|
const sortedData = sortData([...userData], 'user');
|
||||||
|
|
||||||
const table = `
|
const table = `
|
||||||
@ -544,15 +547,15 @@ HTML_TEMPLATE = """
|
|||||||
function sortData(data, type) {
|
function sortData(data, type) {
|
||||||
if (type === 'acc') {
|
if (type === 'acc') {
|
||||||
return data.sort((a, b) => {
|
return data.sort((a, b) => {
|
||||||
// 13c prefix always on top
|
// Configured prefix always on top
|
||||||
const aIs13c = a.username && a.username.startsWith('13c');
|
const aIsPreferred = PREFIX_PATTERN && a.username && a.username.startsWith(PREFIX_PATTERN);
|
||||||
const bIs13c = b.username && b.username.startsWith('13c');
|
const bIsPreferred = PREFIX_PATTERN && b.username && b.username.startsWith(PREFIX_PATTERN);
|
||||||
|
|
||||||
if (aIs13c && !bIs13c) return -1;
|
if (aIsPreferred && !bIsPreferred) return -1;
|
||||||
if (!aIs13c && bIs13c) return 1;
|
if (!aIsPreferred && bIsPreferred) return 1;
|
||||||
|
|
||||||
// If both are 13c or both are not 13c, sort by username in descending order
|
// If both are preferred or both are not, sort by username in descending order
|
||||||
if (aIs13c && bIs13c) {
|
if (aIsPreferred && bIsPreferred) {
|
||||||
return (b.username || '').localeCompare(a.username || '');
|
return (b.username || '').localeCompare(a.username || '');
|
||||||
} else {
|
} else {
|
||||||
return (b.username || '').localeCompare(a.username || '');
|
return (b.username || '').localeCompare(a.username || '');
|
||||||
@ -560,12 +563,12 @@ HTML_TEMPLATE = """
|
|||||||
});
|
});
|
||||||
} else if (type === 'user') {
|
} else if (type === 'user') {
|
||||||
return data.sort((a, b) => {
|
return data.sort((a, b) => {
|
||||||
// 13c prefix always on top
|
// Configured prefix always on top
|
||||||
const aIs13c = a.f_username && a.f_username.startsWith('13c');
|
const aIsPreferred = PREFIX_PATTERN && a.f_username && a.f_username.startsWith(PREFIX_PATTERN);
|
||||||
const bIs13c = b.f_username && b.f_username.startsWith('13c');
|
const bIsPreferred = PREFIX_PATTERN && b.f_username && b.f_username.startsWith(PREFIX_PATTERN);
|
||||||
|
|
||||||
if (aIs13c && !bIs13c) return -1;
|
if (aIsPreferred && !bIsPreferred) return -1;
|
||||||
if (!aIs13c && bIs13c) return 1;
|
if (!aIsPreferred && bIsPreferred) return 1;
|
||||||
|
|
||||||
// Then sort by last_update_time (newest first)
|
// Then sort by last_update_time (newest first)
|
||||||
const aTime = new Date(a.last_update_time || 0);
|
const aTime = new Date(a.last_update_time || 0);
|
||||||
@ -702,7 +705,7 @@ HTML_TEMPLATE = """
|
|||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template_string(HTML_TEMPLATE)
|
return render_template_string(HTML_TEMPLATE, prefix_pattern=PREFIX_PATTERN)
|
||||||
|
|
||||||
@app.route('/api/acc/')
|
@app.route('/api/acc/')
|
||||||
def proxy_acc():
|
def proxy_acc():
|
||||||
@ -742,4 +745,4 @@ if __name__ == '__main__':
|
|||||||
print("Starting CM Web View...")
|
print("Starting CM Web View...")
|
||||||
print("Web interface will be available at: http://localhost:8000")
|
print("Web interface will be available at: http://localhost:8000")
|
||||||
print("Make sure the API server is running on port 3000")
|
print("Make sure the API server is running on port 3000")
|
||||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||||
|
|||||||
60
app/db.py
60
app/db.py
@ -1,32 +1,53 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
from mysql.connector import Error
|
from mysql.connector import Error
|
||||||
|
|
||||||
|
|
||||||
|
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 DB:
|
class DB:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.config = {
|
self.config = {
|
||||||
'host': '192.168.0.210',
|
'host': _get_required_env('DB_HOST'),
|
||||||
'user': 'rex_cm',
|
'user': _get_required_env('DB_USER'),
|
||||||
'password': 'hengserver',
|
'password': _get_required_env('DB_PASSWORD'),
|
||||||
'database': 'rex_cm',
|
'database': _get_required_env('DB_NAME'),
|
||||||
'port': 3306
|
'port': int(_get_required_env('DB_PORT')),
|
||||||
|
'connection_timeout': int(_get_required_env('DB_CONNECTION_TIMEOUT'))
|
||||||
}
|
}
|
||||||
|
self.connect_retries = max(1, int(_get_required_env('DB_CONNECT_RETRIES')))
|
||||||
|
self.connect_retry_delay = float(_get_required_env('DB_CONNECT_RETRY_DELAY'))
|
||||||
self.init_database()
|
self.init_database()
|
||||||
|
|
||||||
def get_connection(self):
|
def get_connection(self):
|
||||||
"""Get MySQL database connection."""
|
"""Get MySQL database connection."""
|
||||||
try:
|
for attempt in range(1, self.connect_retries + 1):
|
||||||
connection = mysql.connector.connect(**self.config)
|
try:
|
||||||
return connection
|
connection = mysql.connector.connect(**self.config)
|
||||||
except Error as e:
|
return connection
|
||||||
print(f"Error connecting to MySQL: {e}")
|
except Error as e:
|
||||||
return None
|
print(f"Error connecting to MySQL: {e}")
|
||||||
|
if attempt < self.connect_retries:
|
||||||
|
print(
|
||||||
|
f"Retrying MySQL connection ({attempt}/{self.connect_retries}) "
|
||||||
|
f"in {self.connect_retry_delay} seconds..."
|
||||||
|
)
|
||||||
|
time.sleep(self.connect_retry_delay)
|
||||||
|
return None
|
||||||
|
|
||||||
def init_database(self):
|
def init_database(self):
|
||||||
"""Initialize the database connection."""
|
"""Initialize the database connection."""
|
||||||
connection = self.get_connection()
|
connection = self.get_connection()
|
||||||
if connection is None:
|
if connection is None:
|
||||||
raise Exception("Failed to connect to database")
|
raise Exception("Failed to connect to database")
|
||||||
|
cursor = None
|
||||||
try:
|
try:
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
# Test connection by checking if required tables exist
|
# Test connection by checking if required tables exist
|
||||||
@ -44,8 +65,9 @@ class DB:
|
|||||||
print(f"Error verifying database: {e}")
|
print(f"Error verifying database: {e}")
|
||||||
raise Exception(f"Database verification failed: {e}")
|
raise Exception(f"Database verification failed: {e}")
|
||||||
finally:
|
finally:
|
||||||
if connection.is_connected():
|
if cursor is not None:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
if connection.is_connected():
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|
||||||
def query(self, query, params=None):
|
def query(self, query, params=None):
|
||||||
@ -53,7 +75,7 @@ class DB:
|
|||||||
connection = self.get_connection()
|
connection = self.get_connection()
|
||||||
if connection is None:
|
if connection is None:
|
||||||
return []
|
return []
|
||||||
|
cursor = None
|
||||||
try:
|
try:
|
||||||
cursor = connection.cursor(dictionary=True)
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
|
||||||
@ -69,8 +91,9 @@ class DB:
|
|||||||
print(f"Error executing query: {e}")
|
print(f"Error executing query: {e}")
|
||||||
return []
|
return []
|
||||||
finally:
|
finally:
|
||||||
if connection.is_connected():
|
if cursor is not None:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
if connection.is_connected():
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|
||||||
def execute(self, query, params=None):
|
def execute(self, query, params=None):
|
||||||
@ -78,7 +101,7 @@ class DB:
|
|||||||
connection = self.get_connection()
|
connection = self.get_connection()
|
||||||
if connection is None:
|
if connection is None:
|
||||||
return False
|
return False
|
||||||
|
cursor = None
|
||||||
try:
|
try:
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
|
||||||
@ -94,6 +117,7 @@ class DB:
|
|||||||
print(f"Error executing query: {e}")
|
print(f"Error executing query: {e}")
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
if connection.is_connected():
|
if cursor is not None:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
connection.close()
|
if connection.is_connected():
|
||||||
|
connection.close()
|
||||||
|
|||||||
115
app/telegram_notifier.py
Normal file
115
app/telegram_notifier.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TELEGRAM_API_URL = "https://api.telegram.org"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_env_from_file() -> None:
|
||||||
|
"""
|
||||||
|
Load key=value pairs from a .env-style file to populate os.environ.
|
||||||
|
Existing environment variables take precedence.
|
||||||
|
"""
|
||||||
|
candidate_paths = []
|
||||||
|
env_file = os.getenv("ENV_FILE_PATH")
|
||||||
|
if env_file:
|
||||||
|
candidate_paths.append(Path(env_file))
|
||||||
|
candidate_paths.append(Path(__file__).resolve().parents[1] / ".env")
|
||||||
|
candidate_paths.append(Path.cwd() / ".env")
|
||||||
|
|
||||||
|
for path in candidate_paths:
|
||||||
|
if not path:
|
||||||
|
continue
|
||||||
|
if not path.exists() or not path.is_file():
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
with path.open("r") as env_file_handle:
|
||||||
|
for line in env_file_handle:
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
||||||
|
continue
|
||||||
|
key, value = stripped.split("=", 1)
|
||||||
|
key = key.strip()
|
||||||
|
value = value.strip().strip('"').strip("'")
|
||||||
|
if key and key not in os.environ:
|
||||||
|
os.environ[key] = value
|
||||||
|
except OSError as exc:
|
||||||
|
logger.warning("Unable to load env file %s: %s", path, exc)
|
||||||
|
|
||||||
|
|
||||||
|
_load_env_from_file()
|
||||||
|
|
||||||
|
|
||||||
|
def get_telegram_bot_token() -> str:
|
||||||
|
"""
|
||||||
|
Fetch the Telegram bot token from the TELEGRAM_BOT_TOKEN environment variable.
|
||||||
|
Raises RuntimeError if missing so the application fails fast during startup.
|
||||||
|
"""
|
||||||
|
token = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
|
if not token:
|
||||||
|
_load_env_from_file()
|
||||||
|
token = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
|
if not token:
|
||||||
|
raise RuntimeError("Missing TELEGRAM_BOT_TOKEN environment variable")
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
class TelegramNotifier:
|
||||||
|
"""
|
||||||
|
Utility wrapper to send proactive notifications to a dedicated Telegram chat.
|
||||||
|
Requires TELEGRAM_ALERT_CHAT_ID and TELEGRAM_ALERT_BOT_TOKEN (or TELEGRAM_BOT_TOKEN) env vars.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.chat_id = os.getenv("TELEGRAM_ALERT_CHAT_ID")
|
||||||
|
self.bot_token = os.getenv("TELEGRAM_ALERT_BOT_TOKEN") or os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
|
if not self.chat_id:
|
||||||
|
logger.warning(
|
||||||
|
"TELEGRAM_ALERT_CHAT_ID environment variable missing; skipping Telegram notification setup"
|
||||||
|
)
|
||||||
|
if not self.bot_token:
|
||||||
|
logger.warning(
|
||||||
|
"TELEGRAM_ALERT_BOT_TOKEN/TELEGRAM_BOT_TOKEN env var missing; skipping Telegram notification setup"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _can_notify(self) -> bool:
|
||||||
|
return bool(self.chat_id and self.bot_token)
|
||||||
|
|
||||||
|
def _send_message(self, text: str, parse_mode: Optional[str] = None) -> None:
|
||||||
|
if not self._can_notify():
|
||||||
|
logger.warning(
|
||||||
|
"Skipping Telegram notification: bot_token/chat_id not configured. Message: %s",
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
url = f"{TELEGRAM_API_URL}/bot{self.bot_token}/sendMessage"
|
||||||
|
payload = {"chat_id": self.chat_id, "text": text}
|
||||||
|
if parse_mode:
|
||||||
|
payload["parse_mode"] = parse_mode
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=payload, timeout=10)
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.error(
|
||||||
|
"Failed to send Telegram notification. Status: %s Body: %s",
|
||||||
|
response.status_code,
|
||||||
|
response.text,
|
||||||
|
)
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
logger.exception("Error sending Telegram notification: %s", exc)
|
||||||
|
|
||||||
|
def notify_login_failure(self, details: str) -> None:
|
||||||
|
message = f"⚠️ CM Telegram Bot login issue detected.\nDetails: {details}\nAutomatic account creation paused until the next retry window."
|
||||||
|
self._send_message(message)
|
||||||
|
|
||||||
|
def notify_generic_error(self, details: str) -> None:
|
||||||
|
message = f"⚠️ CM Telegram Bot encountered an issue:\n{details}"
|
||||||
|
self._send_message(message)
|
||||||
@ -23,7 +23,7 @@ services:
|
|||||||
dockerfile: docker/transfer/Dockerfile
|
dockerfile: docker/transfer/Dockerfile
|
||||||
image: "${CM_IMAGE_PREFIX:-local}/cm-transfer:${DOCKER_IMAGE_TAG:-dev}"
|
image: "${CM_IMAGE_PREFIX:-local}/cm-transfer:${DOCKER_IMAGE_TAG:-dev}"
|
||||||
environment:
|
environment:
|
||||||
- API_BASE_URL=http://api-server:3000
|
API_BASE_URL: http://api-server:3000
|
||||||
- CM_TRANSFER_MAX_THREADS=1
|
CM_TRANSFER_MAX_THREADS: "1"
|
||||||
mem_limit: 2g
|
mem_limit: 2g
|
||||||
cpus: 2
|
cpus: 2
|
||||||
|
|||||||
@ -2,71 +2,110 @@ services:
|
|||||||
# Telegram Bot Service
|
# Telegram Bot Service
|
||||||
telegram-bot:
|
telegram-bot:
|
||||||
image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-telegram:${DOCKER_IMAGE_TAG:-latest}"
|
image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-telegram:${DOCKER_IMAGE_TAG:-latest}"
|
||||||
container_name: cm-telegram-bot
|
container_name: ${CM_DEPLOY_NAME:-cm}-telegram-bot
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- PYTHONUNBUFFERED=1
|
PYTHONUNBUFFERED: "1"
|
||||||
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
|
||||||
|
TELEGRAM_ALERT_CHAT_ID: ${TELEGRAM_ALERT_CHAT_ID:-}
|
||||||
|
TELEGRAM_ALERT_BOT_TOKEN: ${TELEGRAM_ALERT_BOT_TOKEN:-}
|
||||||
|
CM_PREFIX_PATTERN: ${CM_PREFIX_PATTERN}
|
||||||
|
CM_AGENT_ID: ${CM_AGENT_ID}
|
||||||
|
CM_AGENT_PASSWORD: ${CM_AGENT_PASSWORD}
|
||||||
|
CM_SECURITY_PIN: ${CM_SECURITY_PIN}
|
||||||
|
CM_BOT_BASE_URL: ${CM_BOT_BASE_URL}
|
||||||
|
DB_HOST: ${DB_HOST}
|
||||||
|
DB_USER: ${DB_USER}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
DB_NAME: ${DB_NAME}
|
||||||
|
DB_PORT: ${DB_PORT}
|
||||||
|
DB_CONNECTION_TIMEOUT: ${DB_CONNECTION_TIMEOUT}
|
||||||
|
DB_CONNECT_RETRIES: ${DB_CONNECT_RETRIES}
|
||||||
|
DB_CONNECT_RETRY_DELAY: ${DB_CONNECT_RETRY_DELAY}
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
networks:
|
networks:
|
||||||
- cm-network
|
- bot-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- api-server
|
- api-server
|
||||||
|
|
||||||
# API Server Service
|
# API Server Service
|
||||||
api-server:
|
api-server:
|
||||||
image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-api:${DOCKER_IMAGE_TAG:-latest}"
|
image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-api:${DOCKER_IMAGE_TAG:-latest}"
|
||||||
container_name: cm-api-server
|
container_name: ${CM_DEPLOY_NAME:-cm}-api-server
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000"
|
||||||
environment:
|
environment:
|
||||||
- PYTHONUNBUFFERED=1
|
PYTHONUNBUFFERED: "1"
|
||||||
|
DB_HOST: ${DB_HOST}
|
||||||
|
DB_USER: ${DB_USER}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
DB_NAME: ${DB_NAME}
|
||||||
|
DB_PORT: ${DB_PORT}
|
||||||
|
DB_CONNECTION_TIMEOUT: ${DB_CONNECTION_TIMEOUT}
|
||||||
|
DB_CONNECT_RETRIES: ${DB_CONNECT_RETRIES}
|
||||||
|
DB_CONNECT_RETRY_DELAY: ${DB_CONNECT_RETRY_DELAY}
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
networks:
|
networks:
|
||||||
- cm-network
|
- bot-network
|
||||||
|
|
||||||
# Web View Service
|
# Web View Service
|
||||||
web-view:
|
web-view:
|
||||||
image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-web:${DOCKER_IMAGE_TAG:-latest}"
|
image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-web:${DOCKER_IMAGE_TAG:-latest}"
|
||||||
container_name: cm-web-view
|
container_name: ${CM_DEPLOY_NAME:-cm}-web-view
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8001:8000"
|
- "${CM_WEB_HOST_PORT:-8001}:8000"
|
||||||
environment:
|
environment:
|
||||||
- PYTHONUNBUFFERED=1
|
PYTHONUNBUFFERED: "1"
|
||||||
- API_BASE_URL=http://api-server:3000
|
API_BASE_URL: http://api-server:3000
|
||||||
|
CM_PREFIX_PATTERN: ${CM_PREFIX_PATTERN}
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
networks:
|
networks:
|
||||||
- cm-network
|
- bot-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- api-server
|
- api-server
|
||||||
|
|
||||||
transfer-bot:
|
transfer-bot:
|
||||||
image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-transfer:${DOCKER_IMAGE_TAG:-latest}"
|
image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-transfer:${DOCKER_IMAGE_TAG:-latest}"
|
||||||
container_name: cm-transfer-bot
|
container_name: ${CM_DEPLOY_NAME:-cm}-transfer-bot
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- PYTHONUNBUFFERED=1
|
PYTHONUNBUFFERED: "1"
|
||||||
- API_BASE_URL=http://api-server:3000
|
API_BASE_URL: http://api-server:3000
|
||||||
- CM_TRANSFER_MAX_THREADS=20
|
CM_TRANSFER_MAX_THREADS: "20"
|
||||||
|
CM_PREFIX_PATTERN: ${CM_PREFIX_PATTERN}
|
||||||
|
CM_AGENT_ID: ${CM_AGENT_ID}
|
||||||
|
CM_AGENT_PASSWORD: ${CM_AGENT_PASSWORD}
|
||||||
|
CM_SECURITY_PIN: ${CM_SECURITY_PIN}
|
||||||
|
CM_BOT_BASE_URL: ${CM_BOT_BASE_URL}
|
||||||
|
DB_HOST: ${DB_HOST}
|
||||||
|
DB_USER: ${DB_USER}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
DB_NAME: ${DB_NAME}
|
||||||
|
DB_PORT: ${DB_PORT}
|
||||||
|
DB_CONNECTION_TIMEOUT: ${DB_CONNECTION_TIMEOUT}
|
||||||
|
DB_CONNECT_RETRIES: ${DB_CONNECT_RETRIES}
|
||||||
|
DB_CONNECT_RETRY_DELAY: ${DB_CONNECT_RETRY_DELAY}
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
mem_limit: 6g
|
mem_limit: 6g
|
||||||
cpus: 2
|
cpus: 2
|
||||||
networks:
|
networks:
|
||||||
- cm-network
|
- bot-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- api-server
|
- api-server
|
||||||
- web-view
|
- web-view
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
cm-network:
|
bot-network:
|
||||||
|
name: ${CM_DEPLOY_NAME:-cm}-network
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
28
envs/rex/.env
Normal file
28
envs/rex/.env
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# === Deployment Identity ===
|
||||||
|
CM_DEPLOY_NAME=rex-cm
|
||||||
|
CM_WEB_HOST_PORT=8001
|
||||||
|
|
||||||
|
# === Docker Registry ===
|
||||||
|
CM_IMAGE_PREFIX=gitea.04080616.xyz/yiekheng
|
||||||
|
DOCKER_IMAGE_TAG=latest
|
||||||
|
|
||||||
|
# === Telegram ===
|
||||||
|
TELEGRAM_BOT_TOKEN=5315819168:AAH31xwNgPdnk123x97XalmTW6fQV5EUCFU
|
||||||
|
TELEGRAM_ALERT_CHAT_ID=818380985
|
||||||
|
|
||||||
|
# === Database ===
|
||||||
|
DB_HOST=192.168.0.210
|
||||||
|
DB_USER=rex_cm
|
||||||
|
DB_PASSWORD=hengserver
|
||||||
|
DB_NAME=rex_cm
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_CONNECTION_TIMEOUT=8
|
||||||
|
DB_CONNECT_RETRIES=5
|
||||||
|
DB_CONNECT_RETRY_DELAY=2
|
||||||
|
|
||||||
|
# === Bot Config ===
|
||||||
|
CM_PREFIX_PATTERN=13c
|
||||||
|
CM_AGENT_ID=cm13a3
|
||||||
|
CM_AGENT_PASSWORD=Sky533535
|
||||||
|
CM_SECURITY_PIN=Sky533535
|
||||||
|
CM_BOT_BASE_URL=https://cm99.net
|
||||||
28
envs/siong/.env
Normal file
28
envs/siong/.env
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# === Deployment Identity ===
|
||||||
|
CM_DEPLOY_NAME=siong-cm
|
||||||
|
CM_WEB_HOST_PORT=8005
|
||||||
|
|
||||||
|
# === Docker Registry ===
|
||||||
|
CM_IMAGE_PREFIX=gitea.04080616.xyz/yiekheng
|
||||||
|
DOCKER_IMAGE_TAG=latest
|
||||||
|
|
||||||
|
# === Telegram ===
|
||||||
|
TELEGRAM_BOT_TOKEN=7028479329:AAH_UTPoYcaB0iZMXJjO7pKYxyub8ZSXn2E
|
||||||
|
TELEGRAM_ALERT_CHAT_ID=818380985
|
||||||
|
|
||||||
|
# === Database ===
|
||||||
|
DB_HOST=192.168.0.210
|
||||||
|
DB_USER=siong_cm
|
||||||
|
DB_PASSWORD=hengserver
|
||||||
|
DB_NAME=siong_cm
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_CONNECTION_TIMEOUT=8
|
||||||
|
DB_CONNECT_RETRIES=5
|
||||||
|
DB_CONNECT_RETRY_DELAY=2
|
||||||
|
|
||||||
|
# === Bot Config ===
|
||||||
|
CM_PREFIX_PATTERN=13sa
|
||||||
|
CM_AGENT_ID=cm13a39
|
||||||
|
CM_AGENT_PASSWORD=Wenwen12345
|
||||||
|
CM_SECURITY_PIN=Wenwen12345
|
||||||
|
CM_BOT_BASE_URL=https://cm99.net
|
||||||
Loading…
x
Reference in New Issue
Block a user