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:
51
.gitea/workflows/docker.yaml
Normal file
51
.gitea/workflows/docker.yaml
Normal 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
21
Dockerfile
Normal 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"]
|
||||||
@@ -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
|
||||||
# Use provided info or fall back to defaults
|
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()
|
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,
|
||||||
@@ -438,17 +479,18 @@ def register_domain(req: DomainRegisterRequest):
|
|||||||
phone=req.phone or default.phone,
|
phone=req.phone or default.phone,
|
||||||
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
40
deposit_api.py
Normal 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()
|
||||||
@@ -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
2
uv.lock
generated
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user