1
0

Integrate ai.card Rust API with ai.gpt MCP and implement daily limit system

### Major Changes:
- **Rust Migration**: Move api-rs to root directory, rename binary to 'aicard'
- **MCP Integration**: Add card tools to ai.gpt MCP server (get_user_cards, draw_card, get_draw_status)
- **Daily Limit System**: Implement 2-day interval card drawing limits in API and iOS
- **iOS Enhancements**: Add DrawStatusView, backup functionality, and limit integration

### Technical Details:
- ai.gpt MCP now has 20 tools including 3 card-related tools
- ServiceClient enhanced with missing card API methods
- iOS app includes daily limit UI and atproto OAuth backup features
- Database migration for last_draw_date field
- Complete feature parity between web and iOS implementations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-06-08 10:35:43 +09:00
parent 2e55e6ce09
commit 9f9208e160
14 changed files with 384 additions and 12 deletions

View File

@ -34,6 +34,9 @@ class Settings(BaseSettings):
# Unique card settings
max_unique_cards: int = 1000 # Maximum number of unique cards
# Draw limit settings
draw_limit_days: int = 2 # Number of days between allowed draws (every other day)
# CORS
cors_origins: list[str] = [
"http://localhost:3000",

View File

@ -23,6 +23,7 @@ class User(Base):
handle = Column(String, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
last_draw_date = Column(DateTime, nullable=True)
# Relationships
cards = relationship("UserCard", back_populates="owner")

View File

@ -1,6 +1,7 @@
"""User repository"""
from typing import Optional
from sqlalchemy import select
from datetime import datetime, timedelta
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@ -35,4 +36,62 @@ class UserRepository(BaseRepository[User]):
.options(selectinload(User.cards))
.where(User.id == user_id)
)
return result.scalar_one_or_none()
return result.scalar_one_or_none()
async def can_draw_card(self, did: str, draw_limit_days: int = 2) -> bool:
"""
Check if user can draw a card based on daily limit
Args:
did: User's DID
draw_limit_days: Number of days between allowed draws (default: 2 days)
Returns:
True if user can draw, False if still in cooldown period
"""
user = await self.get_by_did(did)
if not user:
# New user can always draw
return True
if not user.last_draw_date:
# User has never drawn before
return True
# Check if enough time has passed since last draw
time_since_last_draw = datetime.utcnow() - user.last_draw_date
return time_since_last_draw >= timedelta(days=draw_limit_days)
async def get_next_draw_time(self, did: str, draw_limit_days: int = 2) -> Optional[datetime]:
"""
Get the next time the user can draw a card
Args:
did: User's DID
draw_limit_days: Number of days between allowed draws
Returns:
Next draw time if user is in cooldown, None if user can draw now
"""
user = await self.get_by_did(did)
if not user or not user.last_draw_date:
return None
next_draw_time = user.last_draw_date + timedelta(days=draw_limit_days)
if datetime.utcnow() >= next_draw_time:
return None
return next_draw_time
async def update_last_draw_date(self, user_id: int) -> None:
"""
Update user's last draw date to current time
Args:
user_id: User's database ID
"""
await self.session.execute(
update(User)
.where(User.id == user_id)
.values(last_draw_date=datetime.utcnow())
)

View File

@ -56,11 +56,38 @@ async def draw_card(
await db.commit()
return result
except ValueError as e:
# Handle daily limit error
await db.rollback()
raise HTTPException(status_code=429, detail=str(e))
except Exception as e:
await db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/draw-status/{user_did}")
async def get_draw_status(
user_did: str,
db: AsyncSession = Depends(get_session)
):
"""
ユーザーのガチャ実行状況を取得
- **user_did**: ユーザーのatproto DID
"""
from app.core.config import settings
user_repo = UserRepository(db)
can_draw = await user_repo.can_draw_card(user_did, settings.draw_limit_days)
next_draw_time = await user_repo.get_next_draw_time(user_did, settings.draw_limit_days)
return {
"can_draw": can_draw,
"next_draw_time": next_draw_time.isoformat() if next_draw_time else None,
"draw_limit_days": settings.draw_limit_days
}
@router.get("/user/{user_did}", response_model=List[Card])
async def get_user_cards(
user_did: str,

View File

@ -34,7 +34,19 @@ class GachaService:
Returns:
(Card, is_unique): 抽選されたカードとuniqueかどうか
Raises:
ValueError: If user cannot draw due to daily limit
"""
# Check if user can draw (daily limit)
can_draw = await self.user_repo.can_draw_card(user_did, settings.draw_limit_days)
if not can_draw:
next_draw_time = await self.user_repo.get_next_draw_time(user_did, settings.draw_limit_days)
if next_draw_time:
raise ValueError(f"カードを引けるのは{next_draw_time.strftime('%Y-%m-%d %H:%M:%S')}以降です。")
else:
raise ValueError("現在カードを引くことができません。")
# Get or create user
user = await self.user_repo.get_or_create(user_did)
# レアリティ抽選
@ -78,6 +90,9 @@ class GachaService:
)
self.session.add(draw_history)
# Update user's last draw date
await self.user_repo.update_last_draw_date(user.id)
# API用のCardモデルに変換
card = Card(
id=card_id,