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 __pycache__/
.git *.py[cod]
.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
*.log *.log
.git .git
.mypy_cache logs/
.pytest_cache node_modules/
.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

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 import threading
from flask import Flask, jsonify, request from flask import Flask, jsonify, request
from flask_cors import CORS from flask_cors import CORS
from db import DB from .db import DB
class CM_API: class CM_API:

View File

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

View File

@ -3,7 +3,7 @@ import threading, logging, time, asyncio
from telegram import ForceReply, Update from telegram import ForceReply, Update
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters 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( 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

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

View File

@ -1,14 +1,14 @@
FROM python:3.9-slim FROM python:3.9-slim
ENV PIP_DEFAULT_TIMEOUT=120
WORKDIR /app WORKDIR /app
RUN apt-get update
COPY requirements.txt . 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 application files
COPY . . COPY app ./app
# Set environment variables # Set environment variables
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
@ -17,4 +17,4 @@ ENV PYTHONUNBUFFERED=1
EXPOSE 3000 EXPOSE 3000
# Run the API server # 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 FROM python:3.9-slim
ENV PIP_DEFAULT_TIMEOUT=120
WORKDIR /app WORKDIR /app
RUN apt-get update
COPY requirements.txt . 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 application files
COPY . . COPY app ./app
# Set environment variables # Set environment variables
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
# Run the telegram bot # 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 FROM python:3.9-slim
ENV PIP_DEFAULT_TIMEOUT=120
WORKDIR /app WORKDIR /app
RUN apt-get update
COPY requirements.txt . 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 application files
COPY . . COPY app ./app
# Set environment variables # Set environment variables
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
@ -17,4 +17,4 @@ ENV PYTHONUNBUFFERED=1
EXPOSE 8000 EXPOSE 8000
# Run the web view # 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}'."