Refactor Docker layout for Gitea publishing

- move Python sources into app package and switch services to module entrypoints
- relocate Dockerfiles under docker/, add buildx publish script, override compose for local builds
- configure images to pull from gitea.04080616.xyz/yiekheng with env-driven tags and limits
- harden installs and transfer worker logging/concurrency for cleaner container output
This commit is contained in:
yiekheng 2025-10-19 22:22:55 +08:00
parent d354601329
commit d73439698a
20 changed files with 232 additions and 164 deletions

View File

@ -1,55 +1,6 @@
# Git
.git
.gitignore
# Python
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env
pip-log.txt
pip-delete-this-directory.txt
.tox
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
__pycache__/
*.py[cod]
*.log
.git
.mypy_cache
.pytest_cache
.hypothesis
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IDE
.vscode
.idea
*.swp
*.swo
*~
# Docker
Dockerfile*
docker-compose*.yml
.dockerignore
# Documentation
README.md
*.md
# Other
.env
.env.local
.env.*.local
logs/
node_modules/

2
.env Normal file
View File

@ -0,0 +1,2 @@
CM_IMAGE_PREFIX=gitea.04080616.xyz/yiekheng
DOCKER_IMAGE_TAG=latest

View File

@ -1,17 +0,0 @@
FROM python:3.9-slim
WORKDIR /app
RUN apt-get update
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application files
COPY . .
# Set environment variables
ENV PYTHONUNBUFFERED=1
# Run the API server
CMD ["python", "cm_transfer_credit.py"]

2
app/__init__.py Normal file
View File

@ -0,0 +1,2 @@
"""Shared CM bot application package."""

View File

@ -1,7 +1,7 @@
import threading
from flask import Flask, jsonify, request
from flask_cors import CORS
from db import DB
from .db import DB
class CM_API:
@ -188,4 +188,4 @@ class CM_API:
if __name__ == '__main__':
api = CM_API()
api.run(port = 3000)
api.run(port = 3000)

View File

@ -1,7 +1,7 @@
import requests, re
from bs4 import BeautifulSoup
from cm_bot import CM_BOT
from db import DB
from .cm_bot import CM_BOT
from .db import DB
import secrets, string

View File

@ -3,7 +3,7 @@ import threading, logging, time, asyncio
from telegram import ForceReply, Update
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
from cm_bot_hal import CM_BOT_HAL
from .cm_bot_hal import CM_BOT_HAL
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
@ -107,4 +107,4 @@ def main() -> None:
application.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()
main()

65
app/cm_transfer_credit.py Normal file
View File

@ -0,0 +1,65 @@
import logging, time, requests, json, os, threading
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from .cm_bot_hal import CM_BOT_HAL
# Suppress httpx logs
logging.getLogger("httpx").setLevel(logging.WARNING)
# api_url = 'https://api.luckytown888.net'
api_url = os.getenv('API_BASE_URL', 'http://api-server:3000')
max_threading = int(os.getenv('CM_TRANSFER_MAX_THREADS', '20'))
def transfer(data: dict, local_logger):
bot = CM_BOT_HAL()
thread_name = threading.current_thread().name
local_logger.info(f"[Thread-{thread_name}] [Start] Transfer Credit from {data['f_username']} to {data['t_username']}")
result = bot.transfer_credit_api(data['f_username'], data['f_password'], data['t_username'], data['t_password'])
local_logger.info(f"[Thread-{thread_name}] {result}")
local_logger.info(f"[Thread-{thread_name}] [Done] {data['f_username']} transfer done!")
del bot
time.sleep(5)
def main():
while True:
weekday = int(datetime.now().strftime("%w"))
hour = int(datetime.now().strftime("%H"))
if weekday == 1 and (hour >= 6 and hour < 12):
local_logger = logging.getLogger(__name__)
if not local_logger.handlers:
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
local_logger.addHandler(handler)
local_logger.setLevel(logging.INFO)
local_logger.info("=" * 80)
local_logger.info(
"Transfer window triggered | weekday=%s | hour=%s | max_threads=%s | api_url=%s",
weekday,
hour,
max_threading,
api_url,
)
local_logger.info("=" * 80)
response = requests.get(f'{api_url}/user')
items = json.loads(response.text)
total_items = len(items) if isinstance(items, list) else 0
if total_items == 0:
local_logger.info("No items to process.")
local_logger.info("=" * 80)
else:
local_logger.info(f"Processing {total_items} transfer items...")
with ThreadPoolExecutor(max_workers=max_threading) as executor:
list(executor.map(lambda item: transfer(item, local_logger), items))
local_logger.info(f"Completed processing {total_items} transfer items.")
local_logger.info("=" * 80)
time.sleep(10 * 60)
if __name__ == "__main__":
main()

View File

View File

@ -1,61 +0,0 @@
import logging, time, requests, json, os, threading
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from cm_bot_hal import CM_BOT_HAL
# Suppress httpx logs
logging.getLogger("httpx").setLevel(logging.WARNING)
# Create logs directory if it doesn't exist
logs_dir = "logs"
if not os.path.exists(logs_dir):
os.makedirs(logs_dir)
# api_url = 'https://api.luckytown888.net'
api_url = 'http://api-server:3000'
max_threading = 20
def transfer(data: dict, local_logger):
bot = CM_BOT_HAL()
thread_name = threading.current_thread().name
local_logger.info(f"[Thread-{thread_name}] [Start] Transfer Credit from {data['f_username']} to {data['t_username']}")
result = bot.transfer_credit_api(data['f_username'], data['f_password'], data['t_username'], data['t_password'])
local_logger.info(f"[Thread-{thread_name}] {result}")
local_logger.info(f"[Thread-{thread_name}] [Done] {data['f_username']} transfer done!")
del bot
time.sleep(5)
while True:
weekday = int(datetime.now().strftime("%w"))
hour = int(datetime.now().strftime("%H"))
minutes = int(datetime.now().strftime("%M"))
if weekday == 1 and (hour >= 6 and hour < 12):
local_logger = logging.getLogger(f"{__name__}")
# Configure file handler for logging to logs folder
log_date_dir = f"{logs_dir}/{datetime.now().strftime('%Y%m%d')}"
if not os.path.exists(log_date_dir):
os.makedirs(log_date_dir)
log_filename = f"{log_date_dir}/transfer_credit_{datetime.now().strftime('%Y%m%d_%H%M')}.log"
file_handler = logging.FileHandler(log_filename)
file_handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
# Add file handler to logger if not already added
if not any(isinstance(handler, logging.FileHandler) for handler in local_logger.handlers):
local_logger.addHandler(file_handler)
local_logger.setLevel(logging.INFO)
response = requests.get(f'{api_url}/user')
items = json.loads(response.text)
total_items = len(items) if isinstance(items, list) else 0
if total_items == 0:
local_logger.info("No items to process.")
else:
local_logger.info(f"Processing {total_items} transfer items...")
with ThreadPoolExecutor(max_workers=max_threading) as executor:
results = list(executor.map(lambda item: transfer(item, local_logger), items))
local_logger.info(f"Completed processing {total_items} transfer items.")
time.sleep(10 * 60)

View File

@ -0,0 +1,29 @@
services:
telegram-bot:
build:
context: .
dockerfile: docker/telegram/Dockerfile
image: "${CM_IMAGE_PREFIX:-local}/cm-telegram:${DOCKER_IMAGE_TAG:-dev}"
api-server:
build:
context: .
dockerfile: docker/api/Dockerfile
image: "${CM_IMAGE_PREFIX:-local}/cm-api:${DOCKER_IMAGE_TAG:-dev}"
web-view:
build:
context: .
dockerfile: docker/web/Dockerfile
image: "${CM_IMAGE_PREFIX:-local}/cm-web:${DOCKER_IMAGE_TAG:-dev}"
transfer-bot:
build:
context: .
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
mem_limit: 2g
cpus: 2

View File

@ -1,9 +1,7 @@
services:
# Telegram Bot Service
telegram-bot:
build:
context: .
dockerfile: Dockerfile.telegram
image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-telegram:${DOCKER_IMAGE_TAG:-latest}"
container_name: cm-telegram-bot
restart: unless-stopped
environment:
@ -18,9 +16,7 @@ services:
# API Server Service
api-server:
build:
context: .
dockerfile: Dockerfile.api
image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-api:${DOCKER_IMAGE_TAG:-latest}"
container_name: cm-api-server
restart: unless-stopped
ports:
@ -35,9 +31,7 @@ services:
# Web View Service
web-view:
build:
context: .
dockerfile: Dockerfile.web
image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-web:${DOCKER_IMAGE_TAG:-latest}"
container_name: cm-web-view
restart: unless-stopped
ports:
@ -54,16 +48,18 @@ services:
- api-server
transfer-bot:
build:
context: .
dockerfile: Dockerfile.transfer
image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-transfer:${DOCKER_IMAGE_TAG:-latest}"
container_name: cm-transfer-bot
restart: unless-stopped
environment:
- PYTHONUNBUFFERED=1
- API_BASE_URL=http://api-server:3000
- CM_TRANSFER_MAX_THREADS=20
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
mem_limit: 6g
cpus: 2
networks:
- cm-network
depends_on:

View File

@ -1,14 +1,14 @@
FROM python:3.9-slim
ENV PIP_DEFAULT_TIMEOUT=120
WORKDIR /app
RUN apt-get update
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir --retries 5 -r requirements.txt
# Copy application files
COPY . .
COPY app ./app
# Set environment variables
ENV PYTHONUNBUFFERED=1
@ -17,4 +17,4 @@ ENV PYTHONUNBUFFERED=1
EXPOSE 3000
# Run the API server
CMD ["python", "cm_api.py"]
CMD ["python", "-m", "app.cm_api"]

View File

@ -1,17 +1,17 @@
FROM python:3.9-slim
ENV PIP_DEFAULT_TIMEOUT=120
WORKDIR /app
RUN apt-get update
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir --retries 5 -r requirements.txt
# Copy application files
COPY . .
COPY app ./app
# Set environment variables
ENV PYTHONUNBUFFERED=1
# Run the telegram bot
CMD ["python", "cm_telegram.py"]
CMD ["python", "-m", "app.cm_telegram"]

View File

@ -0,0 +1,17 @@
FROM python:3.9-slim
ENV PIP_DEFAULT_TIMEOUT=120
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --retries 5 -r requirements.txt
# Copy application files
COPY app ./app
# Set environment variables
ENV PYTHONUNBUFFERED=1
# Run the transfer credit worker
CMD ["python", "-m", "app.cm_transfer_credit"]

View File

@ -1,14 +1,14 @@
FROM python:3.9-slim
ENV PIP_DEFAULT_TIMEOUT=120
WORKDIR /app
RUN apt-get update
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir --retries 5 -r requirements.txt
# Copy application files
COPY . .
COPY app ./app
# Set environment variables
ENV PYTHONUNBUFFERED=1
@ -17,4 +17,4 @@ ENV PYTHONUNBUFFERED=1
EXPOSE 8000
# Run the web view
CMD ["python", "cm_web_view.py"]
CMD ["python", "-m", "app.cm_web_view"]

1
scripts/local_build.sh Executable file
View File

@ -0,0 +1 @@
sudo docker compose -f docker-compose.yml -f docker-compose.override.yml up --build

83
scripts/publish.sh Executable file
View File

@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -euo pipefail
REGISTRY_PREFIX="gitea.04080616.xyz/yiekheng"
usage() {
cat <<'EOF'
Build and push CM Bot service images to gitea.04080616.xyz/yiekheng.
Usage:
scripts/publish.sh [tag]
Arguments:
tag Optional tag to publish (default: latest). Override with DOCKER_IMAGE_TAG.
Environment:
DOCKER_IMAGE_TAG Alternative way to set the tag (overrides CLI argument).
BUILD_ARGS Extra arguments passed to each docker build command.
Make sure you are authenticated first:
docker login gitea.04080616.xyz
EOF
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
if ! docker info >/dev/null 2>&1; then
echo "Docker daemon is not reachable. Please start Docker and retry." >&2
exit 1
fi
if ! docker system info --format '{{json .IndexServerAddress}}' | grep -q "gitea.04080616.xyz" 2>/dev/null; then
cat <<'EOF' >&2
Reminder: run 'docker login gitea.04080616.xyz' before publishing so pushes succeed.
EOF
fi
IMAGE_TAG="${1:-${DOCKER_IMAGE_TAG:-latest}}"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PLATFORMS="${CM_IMAGE_PLATFORMS:-linux/amd64}"
if ! docker buildx version >/dev/null 2>&1; then
cat <<'EOF' >&2
Docker Buildx is required for producing registry-compatible images.
Install/enable buildx and rerun, for example:
docker buildx create --use --name cm-bot-builder
docker buildx inspect --bootstrap
EOF
exit 1
fi
echo "Using buildx with platforms: ${PLATFORMS}"
echo
SERVICES=(
"api docker/api/Dockerfile"
"telegram docker/telegram/Dockerfile"
"web docker/web/Dockerfile"
"transfer docker/transfer/Dockerfile"
)
echo "Publishing CM Bot images to ${REGISTRY_PREFIX}/cm-<service>:${IMAGE_TAG}"
echo
for ENTRY in "${SERVICES[@]}"; do
SERVICE="${ENTRY%% *}"
DOCKERFILE="${ENTRY#* }"
IMAGE_NAME="${REGISTRY_PREFIX}/cm-${SERVICE}:${IMAGE_TAG}"
echo "==> Building and pushing ${IMAGE_NAME} (${DOCKERFILE})"
docker buildx build ${BUILD_ARGS:-} \
--platform "${PLATFORMS}" \
-f "${ROOT_DIR}/${DOCKERFILE}" \
-t "${IMAGE_NAME}" \
--push \
"${ROOT_DIR}"
echo
done
echo "All images pushed to ${REGISTRY_PREFIX} with tag '${IMAGE_TAG}'."