From e171440ffd9fb0eac7cc852f2f092ab6a98f5834 Mon Sep 17 00:00:00 2001 From: kaffa Date: Tue, 3 Feb 2026 00:25:10 +0900 Subject: [PATCH] Add Docker build with Gitea Actions CI - Add Dockerfile with Python 3.13 + uv - Add Gitea Actions workflow for auto-build on push - Add deposit_api.py for balance management - Update api_server.py with domain registration endpoint Co-Authored-By: Claude Opus 4.5 --- .gitea/workflows/docker.yaml | 51 ++++++++++++++++++++++ Dockerfile | 21 +++++++++ api_server.py | 83 ++++++++++++++++++++++++++++++------ deposit_api.py | 40 +++++++++++++++++ pyproject.toml | 1 + uv.lock | 2 + 6 files changed, 185 insertions(+), 13 deletions(-) create mode 100644 .gitea/workflows/docker.yaml create mode 100644 Dockerfile create mode 100644 deposit_api.py diff --git a/.gitea/workflows/docker.yaml b/.gitea/workflows/docker.yaml new file mode 100644 index 0000000..6f097b1 --- /dev/null +++ b/.gitea/workflows/docker.yaml @@ -0,0 +1,51 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - master + - main + tags: + - 'v*' + +env: + REGISTRY: ${{ gitea.server_url }} + IMAGE_NAME: ${{ gitea.repository }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.GITEA_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix= + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d4d349e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.13-slim + +WORKDIR /app + +# Install uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +# Copy dependency files first (for layer caching) +COPY pyproject.toml uv.lock ./ + +# Install dependencies +RUN uv sync --frozen --no-dev + +# Copy application code +COPY *.py ./ + +# Expose port +EXPOSE 8000 + +# Run the API server +CMD ["uv", "run", "python", "api_server.py"] diff --git a/api_server.py b/api_server.py index 1d46a7b..a8a3554 100644 --- a/api_server.py +++ b/api_server.py @@ -383,6 +383,7 @@ class DomainRegisterRequest(BaseModel): domain: str years: int = 1 add_whois_guard: bool = True + telegram_id: Optional[str] = None # 예치금 결제 시 필수 # Registrant info (optional - uses default if not provided) first_name: Optional[str] = None last_name: Optional[str] = None @@ -415,15 +416,55 @@ def get_default_registrant() -> RegistrantInfo: @app.post("/domains/register") -def register_domain(req: DomainRegisterRequest): +async def register_domain(req: DomainRegisterRequest): """ - Register a new domain. WARNING: This will charge your account. - - If registrant info is not provided, uses default from environment. + Register a new domain with deposit balance check. + + - telegram_id: Required for deposit payment + - Checks balance before registration + - Deducts from deposit after successful registration """ from namecheap import NamecheapError - - # Use provided info or fall back to defaults + from deposit_api import get_balance, deduct_balance + from db import get_prices, init_db + + # telegram_id 필수 체크 + if not req.telegram_id: + raise HTTPException( + status_code=400, + detail="telegram_id is required for domain registration" + ) + + # 1. 도메인 가격 조회 + init_db() + tld = req.domain.rsplit(".", 1)[-1] if "." in req.domain else req.domain + prices = get_prices() + price_info = next((p for p in prices if p["tld"] == tld), None) + + if not price_info: + raise HTTPException( + status_code=400, + detail=f"TLD '{tld}' pricing not found" + ) + + price_krw = price_info["krw"] * req.years + + # 2. 예치금 잔액 확인 + balance_result = await get_balance(req.telegram_id) + if "error" in balance_result: + raise HTTPException( + status_code=400, + detail=f"잔액 조회 실패: {balance_result['error']}" + ) + + current_balance = balance_result.get("balance", 0) + if current_balance < price_krw: + raise HTTPException( + status_code=400, + detail=f"잔액 부족: 현재 {current_balance:,}원, 필요 {price_krw:,}원" + ) + + # 3. Registrant 정보 준비 default = get_default_registrant() registrant = RegistrantInfo( first_name=req.first_name or default.first_name, @@ -438,17 +479,18 @@ def register_domain(req: DomainRegisterRequest): phone=req.phone or default.phone, email=req.email or default.email, ) - - # Validate required fields - required = ["first_name", "last_name", "address1", "city", "state_province", + + # 필수 필드 체크 + required = ["first_name", "last_name", "address1", "city", "state_province", "postal_code", "country", "phone", "email"] missing = [f for f in required if not getattr(registrant, f)] if missing: raise HTTPException( - status_code=400, - detail=f"Missing required registrant fields: {', '.join(missing)}. Set DEFAULT_* env vars or provide in request." + status_code=400, + detail=f"Missing required registrant fields: {', '.join(missing)}. Set REGISTRANT_* env vars." ) - + + # 4. 도메인 등록 try: result = api.domains_create( domain=req.domain, @@ -456,10 +498,25 @@ def register_domain(req: DomainRegisterRequest): years=req.years, add_whois_guard=req.add_whois_guard, ) - return result except NamecheapError as e: raise HTTPException(status_code=400, detail=str(e)) + # 5. 등록 성공 시 예치금 차감 + if result.get("registered"): + deduct_result = await deduct_balance( + telegram_id=req.telegram_id, + amount=price_krw, + reason=f"도메인 등록: {req.domain} ({req.years}년)" + ) + + if "error" in deduct_result: + # 차감 실패 시 경고 (도메인은 이미 등록됨) + result["warning"] = f"도메인은 등록되었으나 예치금 차감 실패: {deduct_result['error']}" + else: + result["deposit_deducted"] = price_krw + result["new_balance"] = deduct_result.get("new_balance", 0) + + return result if __name__ == "__main__": import uvicorn diff --git a/deposit_api.py b/deposit_api.py new file mode 100644 index 0000000..a16b465 --- /dev/null +++ b/deposit_api.py @@ -0,0 +1,40 @@ +""" +Deposit API Client - telegram-bot-workers deposit API 연동 +""" + +import os +import httpx +from dotenv import load_dotenv + +load_dotenv() + +DEPOSIT_API_URL = os.getenv("DEPOSIT_API_URL", "https://telegram-summary-bot.kappa-d8e.workers.dev") +DEPOSIT_API_SECRET = os.getenv("DEPOSIT_API_SECRET", "") + + +async def get_balance(telegram_id: str) -> dict: + """사용자 예치금 잔액 조회""" + async with httpx.AsyncClient() as client: + response = await client.get( + f"{DEPOSIT_API_URL}/api/deposit/balance", + params={"telegram_id": telegram_id}, + headers={"X-API-Key": DEPOSIT_API_SECRET}, + timeout=10.0, + ) + return response.json() + + +async def deduct_balance(telegram_id: str, amount: int, reason: str) -> dict: + """사용자 예치금 차감""" + async with httpx.AsyncClient() as client: + response = await client.post( + f"{DEPOSIT_API_URL}/api/deposit/deduct", + json={ + "telegram_id": telegram_id, + "amount": amount, + "reason": reason, + }, + headers={"X-API-Key": DEPOSIT_API_SECRET}, + timeout=10.0, + ) + return response.json() diff --git a/pyproject.toml b/pyproject.toml index d4e77b6..ba06b46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "fastapi>=0.128.0", + "httpx>=0.28.1", "mcp>=1.25.0", "python-dotenv>=1.2.1", "requests>=2.32.5", diff --git a/uv.lock b/uv.lock index 49a28c5..c7235cd 100644 --- a/uv.lock +++ b/uv.lock @@ -341,6 +341,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "fastapi" }, + { name = "httpx" }, { name = "mcp" }, { name = "python-dotenv" }, { name = "requests" }, @@ -349,6 +350,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "fastapi", specifier = ">=0.128.0" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "mcp", specifier = ">=1.25.0" }, { name = "python-dotenv", specifier = ">=1.2.1" }, { name = "requests", specifier = ">=2.32.5" },