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 <noreply@anthropic.com>
This commit is contained in:
kaffa
2026-02-03 00:25:10 +09:00
parent 050fb8205e
commit e171440ffd
6 changed files with 185 additions and 13 deletions

View File

@@ -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

21
Dockerfile Normal file
View File

@@ -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"]

View File

@@ -383,6 +383,7 @@ class DomainRegisterRequest(BaseModel):
domain: str domain: str
years: int = 1 years: int = 1
add_whois_guard: bool = True add_whois_guard: bool = True
telegram_id: Optional[str] = None # 예치금 결제 시 필수
# Registrant info (optional - uses default if not provided) # Registrant info (optional - uses default if not provided)
first_name: Optional[str] = None first_name: Optional[str] = None
last_name: Optional[str] = None last_name: Optional[str] = None
@@ -415,15 +416,55 @@ def get_default_registrant() -> RegistrantInfo:
@app.post("/domains/register") @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. Register a new domain with deposit balance check.
If registrant info is not provided, uses default from environment. - telegram_id: Required for deposit payment
- Checks balance before registration
- Deducts from deposit after successful registration
""" """
from namecheap import NamecheapError from namecheap import NamecheapError
from deposit_api import get_balance, deduct_balance
from db import get_prices, init_db
# Use provided info or fall back to defaults # 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() default = get_default_registrant()
registrant = RegistrantInfo( registrant = RegistrantInfo(
first_name=req.first_name or default.first_name, first_name=req.first_name or default.first_name,
@@ -439,16 +480,17 @@ def register_domain(req: DomainRegisterRequest):
email=req.email or default.email, 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"] "postal_code", "country", "phone", "email"]
missing = [f for f in required if not getattr(registrant, f)] missing = [f for f in required if not getattr(registrant, f)]
if missing: if missing:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail=f"Missing required registrant fields: {', '.join(missing)}. Set DEFAULT_* env vars or provide in request." detail=f"Missing required registrant fields: {', '.join(missing)}. Set REGISTRANT_* env vars."
) )
# 4. 도메인 등록
try: try:
result = api.domains_create( result = api.domains_create(
domain=req.domain, domain=req.domain,
@@ -456,10 +498,25 @@ def register_domain(req: DomainRegisterRequest):
years=req.years, years=req.years,
add_whois_guard=req.add_whois_guard, add_whois_guard=req.add_whois_guard,
) )
return result
except NamecheapError as e: except NamecheapError as e:
raise HTTPException(status_code=400, detail=str(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__": if __name__ == "__main__":
import uvicorn import uvicorn

40
deposit_api.py Normal file
View File

@@ -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()

View File

@@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"fastapi>=0.128.0", "fastapi>=0.128.0",
"httpx>=0.28.1",
"mcp>=1.25.0", "mcp>=1.25.0",
"python-dotenv>=1.2.1", "python-dotenv>=1.2.1",
"requests>=2.32.5", "requests>=2.32.5",

2
uv.lock generated
View File

@@ -341,6 +341,7 @@ version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "fastapi" }, { name = "fastapi" },
{ name = "httpx" },
{ name = "mcp" }, { name = "mcp" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "requests" }, { name = "requests" },
@@ -349,6 +350,7 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "fastapi", specifier = ">=0.128.0" }, { name = "fastapi", specifier = ">=0.128.0" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "mcp", specifier = ">=1.25.0" }, { name = "mcp", specifier = ">=1.25.0" },
{ name = "python-dotenv", specifier = ">=1.2.1" }, { name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "requests", specifier = ">=2.32.5" }, { name = "requests", specifier = ">=2.32.5" },