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 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()
|
||||
main()
|
||||
|
||||
@ -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('<from_user>', '<from_password>', '<to_user>', '<to_pin>')
|
||||
# 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('<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.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 <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))
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 = """
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const PREFIX_PATTERN = {{ prefix_pattern|tojson }};
|
||||
let currentTab = 'acc';
|
||||
let accData = [];
|
||||
let userData = [];
|
||||
@ -440,7 +443,7 @@ HTML_TEMPLATE = """
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort data with 13c prefix priority
|
||||
// Sort data with configured prefix priority
|
||||
const sortedData = sortData([...accData], 'acc');
|
||||
|
||||
const table = `
|
||||
@ -487,7 +490,7 @@ HTML_TEMPLATE = """
|
||||
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 table = `
|
||||
@ -544,15 +547,15 @@ HTML_TEMPLATE = """
|
||||
function sortData(data, type) {
|
||||
if (type === 'acc') {
|
||||
return data.sort((a, b) => {
|
||||
// 13c prefix always on top
|
||||
const aIs13c = a.username && a.username.startsWith('13c');
|
||||
const bIs13c = b.username && b.username.startsWith('13c');
|
||||
// Configured prefix always on top
|
||||
const aIsPreferred = PREFIX_PATTERN && a.username && a.username.startsWith(PREFIX_PATTERN);
|
||||
const bIsPreferred = PREFIX_PATTERN && b.username && b.username.startsWith(PREFIX_PATTERN);
|
||||
|
||||
if (aIs13c && !bIs13c) return -1;
|
||||
if (!aIs13c && bIs13c) return 1;
|
||||
if (aIsPreferred && !bIsPreferred) return -1;
|
||||
if (!aIsPreferred && bIsPreferred) return 1;
|
||||
|
||||
// If both are 13c or both are not 13c, sort by username in descending order
|
||||
if (aIs13c && bIs13c) {
|
||||
// If both are preferred or both are not, sort by username in descending order
|
||||
if (aIsPreferred && bIsPreferred) {
|
||||
return (b.username || '').localeCompare(a.username || '');
|
||||
} else {
|
||||
return (b.username || '').localeCompare(a.username || '');
|
||||
@ -560,12 +563,12 @@ HTML_TEMPLATE = """
|
||||
});
|
||||
} else if (type === 'user') {
|
||||
return data.sort((a, b) => {
|
||||
// 13c prefix always on top
|
||||
const aIs13c = a.f_username && a.f_username.startsWith('13c');
|
||||
const bIs13c = b.f_username && b.f_username.startsWith('13c');
|
||||
// Configured prefix always on top
|
||||
const aIsPreferred = PREFIX_PATTERN && a.f_username && a.f_username.startsWith(PREFIX_PATTERN);
|
||||
const bIsPreferred = PREFIX_PATTERN && b.f_username && b.f_username.startsWith(PREFIX_PATTERN);
|
||||
|
||||
if (aIs13c && !bIs13c) return -1;
|
||||
if (!aIs13c && bIs13c) return 1;
|
||||
if (aIsPreferred && !bIsPreferred) return -1;
|
||||
if (!aIsPreferred && bIsPreferred) return 1;
|
||||
|
||||
// Then sort by last_update_time (newest first)
|
||||
const aTime = new Date(a.last_update_time || 0);
|
||||
@ -702,7 +705,7 @@ HTML_TEMPLATE = """
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template_string(HTML_TEMPLATE)
|
||||
return render_template_string(HTML_TEMPLATE, prefix_pattern=PREFIX_PATTERN)
|
||||
|
||||
@app.route('/api/acc/')
|
||||
def proxy_acc():
|
||||
@ -742,4 +745,4 @@ if __name__ == '__main__':
|
||||
print("Starting CM Web View...")
|
||||
print("Web interface will be available at: http://localhost:8000")
|
||||
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
|
||||
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:
|
||||
def __init__(self):
|
||||
self.config = {
|
||||
'host': '192.168.0.210',
|
||||
'user': 'rex_cm',
|
||||
'password': 'hengserver',
|
||||
'database': 'rex_cm',
|
||||
'port': 3306
|
||||
'host': _get_required_env('DB_HOST'),
|
||||
'user': _get_required_env('DB_USER'),
|
||||
'password': _get_required_env('DB_PASSWORD'),
|
||||
'database': _get_required_env('DB_NAME'),
|
||||
'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()
|
||||
|
||||
def get_connection(self):
|
||||
"""Get MySQL database connection."""
|
||||
try:
|
||||
connection = mysql.connector.connect(**self.config)
|
||||
return connection
|
||||
except Error as e:
|
||||
print(f"Error connecting to MySQL: {e}")
|
||||
return None
|
||||
for attempt in range(1, self.connect_retries + 1):
|
||||
try:
|
||||
connection = mysql.connector.connect(**self.config)
|
||||
return connection
|
||||
except Error as e:
|
||||
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):
|
||||
"""Initialize the database connection."""
|
||||
connection = self.get_connection()
|
||||
if connection is None:
|
||||
raise Exception("Failed to connect to database")
|
||||
|
||||
cursor = None
|
||||
try:
|
||||
cursor = connection.cursor()
|
||||
# Test connection by checking if required tables exist
|
||||
@ -44,8 +65,9 @@ class DB:
|
||||
print(f"Error verifying database: {e}")
|
||||
raise Exception(f"Database verification failed: {e}")
|
||||
finally:
|
||||
if connection.is_connected():
|
||||
if cursor is not None:
|
||||
cursor.close()
|
||||
if connection.is_connected():
|
||||
connection.close()
|
||||
|
||||
def query(self, query, params=None):
|
||||
@ -53,7 +75,7 @@ class DB:
|
||||
connection = self.get_connection()
|
||||
if connection is None:
|
||||
return []
|
||||
|
||||
cursor = None
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
@ -69,8 +91,9 @@ class DB:
|
||||
print(f"Error executing query: {e}")
|
||||
return []
|
||||
finally:
|
||||
if connection.is_connected():
|
||||
if cursor is not None:
|
||||
cursor.close()
|
||||
if connection.is_connected():
|
||||
connection.close()
|
||||
|
||||
def execute(self, query, params=None):
|
||||
@ -78,7 +101,7 @@ class DB:
|
||||
connection = self.get_connection()
|
||||
if connection is None:
|
||||
return False
|
||||
|
||||
cursor = None
|
||||
try:
|
||||
cursor = connection.cursor()
|
||||
|
||||
@ -94,6 +117,7 @@ class DB:
|
||||
print(f"Error executing query: {e}")
|
||||
return False
|
||||
finally:
|
||||
if connection.is_connected():
|
||||
if cursor is not None:
|
||||
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
|
||||
image: "${CM_IMAGE_PREFIX:-local}/cm-transfer:${DOCKER_IMAGE_TAG:-dev}"
|
||||
environment:
|
||||
- API_BASE_URL=http://api-server:3000
|
||||
- CM_TRANSFER_MAX_THREADS=1
|
||||
API_BASE_URL: http://api-server:3000
|
||||
CM_TRANSFER_MAX_THREADS: "1"
|
||||
mem_limit: 2g
|
||||
cpus: 2
|
||||
|
||||
@ -2,71 +2,110 @@ services:
|
||||
# Telegram Bot Service
|
||||
telegram-bot:
|
||||
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
|
||||
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:
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
networks:
|
||||
- cm-network
|
||||
- bot-network
|
||||
depends_on:
|
||||
- api-server
|
||||
|
||||
# API Server Service
|
||||
api-server:
|
||||
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
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "3000"
|
||||
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:
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
networks:
|
||||
- cm-network
|
||||
- bot-network
|
||||
|
||||
# Web View Service
|
||||
web-view:
|
||||
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
|
||||
ports:
|
||||
- "8001:8000"
|
||||
- "${CM_WEB_HOST_PORT:-8001}:8000"
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- API_BASE_URL=http://api-server:3000
|
||||
PYTHONUNBUFFERED: "1"
|
||||
API_BASE_URL: http://api-server:3000
|
||||
CM_PREFIX_PATTERN: ${CM_PREFIX_PATTERN}
|
||||
volumes:
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
networks:
|
||||
- cm-network
|
||||
- bot-network
|
||||
depends_on:
|
||||
- api-server
|
||||
|
||||
transfer-bot:
|
||||
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
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- API_BASE_URL=http://api-server:3000
|
||||
- CM_TRANSFER_MAX_THREADS=20
|
||||
PYTHONUNBUFFERED: "1"
|
||||
API_BASE_URL: http://api-server:3000
|
||||
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:
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
mem_limit: 6g
|
||||
cpus: 2
|
||||
networks:
|
||||
- cm-network
|
||||
- bot-network
|
||||
depends_on:
|
||||
- api-server
|
||||
- web-view
|
||||
|
||||
|
||||
networks:
|
||||
cm-network:
|
||||
bot-network:
|
||||
name: ${CM_DEPLOY_NAME:-cm}-network
|
||||
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