Compare commits
27 Commits
refactor-b
...
dev
Author | SHA1 | Date | |
---|---|---|---|
fa9611cc99 | |||
8c4db146c9 | |||
1ae08de28b | |||
6e2742bc09 | |||
7ab955dbb4 | |||
8a57fdad7a | |||
a404377108 | |||
c8d8c65251 | |||
dd71c19c6b | |||
56eaaa4103 | |||
91e54bb4e0 | |||
176320154f | |||
25b0ec0809 | |||
cddd20e203 | |||
b5369ed060 | |||
49dbc664b3 | |||
4bf266e2ba | |||
7131f6613e | |||
d2084e73ee | |||
860b73554c | |||
ac0f58fe68 | |||
b505448f36 | |||
06ac3c01a2 | |||
2d377088b0 | |||
ee8bd8c052 | |||
b851a049b8 | |||
409295358c |
@ -5,37 +5,27 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Create .env file
|
||||||
uses: docker/setup-buildx-action@v2
|
run: |
|
||||||
|
echo "MONGO_URI=${{ secrets.MONGO_URI }}" > /home/server/popa_minecraft_launcher_api/.env
|
||||||
|
echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> /home/server/popa_minecraft_launcher_api/.env
|
||||||
|
echo "FILES_URL=${{ secrets.FILES_URL }}" >> /home/server/popa_minecraft_launcher_api/.env
|
||||||
|
echo "TELEGRAM_BOT_TOKEN=${{ secrets.TELEGRAM_BOT_TOKEN }}" >> /home/server/popa_minecraft_launcher_api/.env
|
||||||
|
echo "API_URL=${{ secrets.API_URL }}" >> /home/server/popa_minecraft_launcher_api/.env
|
||||||
|
|
||||||
- name: Login to Gitea Container Registry
|
- name: Build and deploy
|
||||||
uses: docker/login-action@v2
|
run: |
|
||||||
with:
|
cd /home/server/popa_minecraft_launcher_api
|
||||||
registry: git.popa-popa.ru
|
git reset --hard HEAD
|
||||||
username: ${{ secrets.USERNAME }}
|
git checkout main
|
||||||
password: ${{ secrets.PASSWORD }}
|
git pull
|
||||||
|
docker-compose down -v
|
||||||
- name: Build and push Docker image
|
docker-compose build
|
||||||
uses: docker/build-push-action@v4
|
docker-compose up -d
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: git.popa-popa.ru/DIKER/minecraft-api:latest # Замените username на ваше имя пользователя
|
|
||||||
|
|
||||||
- name: Deploy to server
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.SSH_HOST }}
|
|
||||||
username: ${{ secrets.SSH_USERNAME }}
|
|
||||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
||||||
script: |
|
|
||||||
cd /home/server/minecraft-api/
|
|
||||||
docker pull git.popa-popa.ru/DIKER/minecraft-api:latest
|
|
||||||
docker-compose up -d
|
|
||||||
|
41
app/api/bonuses.py
Normal file
41
app/api/bonuses.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from fastapi import APIRouter, Query, Body
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from app.models.bonus import PurchaseBonus
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/api/bonuses",
|
||||||
|
tags=["Bonuses"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/effects")
|
||||||
|
async def get_user_effects(username: str):
|
||||||
|
"""Получить активные эффекты пользователя для плагина"""
|
||||||
|
from app.services.bonus import BonusService
|
||||||
|
return await BonusService().get_user_active_effects(username)
|
||||||
|
|
||||||
|
@router.get("/types")
|
||||||
|
async def get_bonus_types():
|
||||||
|
"""Получить доступные типы бонусов"""
|
||||||
|
from app.services.bonus import BonusService
|
||||||
|
return await BonusService().list_available_bonuses()
|
||||||
|
|
||||||
|
@router.get("/user/{username}")
|
||||||
|
async def get_user_bonuses(username: str):
|
||||||
|
"""Получить активные бонусы пользователя"""
|
||||||
|
from app.services.bonus import BonusService
|
||||||
|
return await BonusService().get_user_bonuses(username)
|
||||||
|
|
||||||
|
@router.post("/purchase")
|
||||||
|
async def purchase_bonus(purchase_bonus: PurchaseBonus):
|
||||||
|
"""Купить бонус"""
|
||||||
|
from app.services.bonus import BonusService
|
||||||
|
return await BonusService().purchase_bonus(purchase_bonus.username, purchase_bonus.bonus_type_id)
|
||||||
|
|
||||||
|
@router.post("/upgrade")
|
||||||
|
async def upgrade_user_bonus(username: str = Body(...), bonus_id: str = Body(...)):
|
||||||
|
"""Улучшить существующий бонус"""
|
||||||
|
from app.services.bonus import BonusService
|
||||||
|
return await BonusService().upgrade_bonus(username, bonus_id)
|
||||||
|
|
@ -61,3 +61,22 @@ async def submit_item_details(data: dict):
|
|||||||
"""Получить подробные данные о предмете"""
|
"""Получить подробные данные о предмете"""
|
||||||
from app.services.marketplace import MarketplaceService
|
from app.services.marketplace import MarketplaceService
|
||||||
return await MarketplaceService().update_item_details(data["operation_id"], data["item_data"])
|
return await MarketplaceService().update_item_details(data["operation_id"], data["item_data"])
|
||||||
|
|
||||||
|
@router.delete("/items/{item_id}")
|
||||||
|
async def cancel_item_sale(
|
||||||
|
item_id: str,
|
||||||
|
username: str = Query(...)
|
||||||
|
):
|
||||||
|
"""Снять предмет с продажи"""
|
||||||
|
from app.services.marketplace import MarketplaceService
|
||||||
|
return await MarketplaceService().cancel_item_sale(username, item_id)
|
||||||
|
|
||||||
|
@router.put("/items/{item_id}/price")
|
||||||
|
async def update_item_price(
|
||||||
|
item_id: str,
|
||||||
|
new_price: int = Body(..., gt=0),
|
||||||
|
username: str = Body(...)
|
||||||
|
):
|
||||||
|
"""Обновить цену предмета на торговой площадке"""
|
||||||
|
from app.services.marketplace import MarketplaceService
|
||||||
|
return await MarketplaceService().update_item_price(username, item_id, new_price)
|
||||||
|
@ -20,8 +20,8 @@ def api_root():
|
|||||||
"homepage": "https://popa-popa.ru"
|
"homepage": "https://popa-popa.ru"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"skinDomains": ["147.78.65.214"],
|
"skinDomains": ["147.78.65.214", "minecraft.api.popa-popa.ru"],
|
||||||
"capeDomains": ["147.78.65.214"],
|
"capeDomains": ["147.78.65.214", "minecraft.api.popa-popa.ru"],
|
||||||
# Важно - возвращаем ключ как есть, без дополнительной обработки
|
# Важно - возвращаем ключ как есть, без дополнительной обработки
|
||||||
"signaturePublickey": public_key
|
"signaturePublickey": public_key
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from fastapi import APIRouter, HTTPException, Body, Response
|
from fastapi import APIRouter, HTTPException, Body, Response
|
||||||
from app.models.user import UserCreate, UserLogin
|
from app.models.user import UserCreate, UserLogin, VerifyCode
|
||||||
from app.models.request import ValidateRequest
|
from app.models.request import ValidateRequest
|
||||||
from app.services.auth import AuthService
|
from app.services.auth import AuthService
|
||||||
from app.db.database import users_collection, sessions_collection
|
from app.db.database import users_collection, sessions_collection
|
||||||
@ -117,3 +117,15 @@ async def get_user_by_uuid(uuid: str):
|
|||||||
safe_user["total_time_formatted"] = f"{hours}ч {minutes}м {seconds}с"
|
safe_user["total_time_formatted"] = f"{hours}ч {minutes}м {seconds}с"
|
||||||
|
|
||||||
return safe_user
|
return safe_user
|
||||||
|
|
||||||
|
@router.post("/auth/verify_code")
|
||||||
|
async def verify_code(verify_code: VerifyCode):
|
||||||
|
return await AuthService().verify_code(verify_code.username, verify_code.code, verify_code.telegram_chat_id)
|
||||||
|
|
||||||
|
@router.post("/auth/generate_code")
|
||||||
|
async def generate_code(username: str):
|
||||||
|
return await AuthService().generate_code(username)
|
||||||
|
|
||||||
|
@router.get("/auth/verification_status/{username}")
|
||||||
|
async def get_verification_status(username: str):
|
||||||
|
return await AuthService().get_verification_status(username)
|
||||||
|
@ -2,6 +2,7 @@ from motor.motor_asyncio import AsyncIOMotorClient
|
|||||||
from app.core.config import MONGO_URI
|
from app.core.config import MONGO_URI
|
||||||
|
|
||||||
client = AsyncIOMotorClient(MONGO_URI)
|
client = AsyncIOMotorClient(MONGO_URI)
|
||||||
|
print(MONGO_URI)
|
||||||
db = client["minecraft-api"]
|
db = client["minecraft-api"]
|
||||||
|
|
||||||
users_collection = db["users"]
|
users_collection = db["users"]
|
||||||
|
48
app/models/bonus.py
Normal file
48
app/models/bonus.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class PurchaseBonus(BaseModel):
|
||||||
|
username: str
|
||||||
|
bonus_type_id: str
|
||||||
|
|
||||||
|
class BonusEffect(BaseModel):
|
||||||
|
effect_type: str
|
||||||
|
effect_value: float
|
||||||
|
expires_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class BonusType(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
effect_type: str # "experience", "strength", "speed", etc.
|
||||||
|
base_effect_value: float # Базовое значение эффекта (например, 1.0 для +100%)
|
||||||
|
effect_increment: float # Прирост эффекта за уровень (например, 0.1 для +10%)
|
||||||
|
price: int # Базовая цена
|
||||||
|
upgrade_price: int # Цена улучшения за уровень
|
||||||
|
duration: int # Длительность в секундах (0 для бесконечных)
|
||||||
|
max_level: int = 0 # 0 = без ограничения уровней
|
||||||
|
|
||||||
|
class UserTypeBonus(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
effect_type: str
|
||||||
|
effect_value: float
|
||||||
|
level: int
|
||||||
|
purchased_at: datetime
|
||||||
|
can_upgrade: bool
|
||||||
|
upgrade_price: int
|
||||||
|
expires_at: Optional[datetime] = None
|
||||||
|
is_active: bool = True
|
||||||
|
is_permanent: bool
|
||||||
|
|
||||||
|
class UserBonus(BaseModel):
|
||||||
|
id: str
|
||||||
|
user_id: str
|
||||||
|
username: str
|
||||||
|
bonus_type_id: str
|
||||||
|
level: int
|
||||||
|
purchased_at: datetime
|
||||||
|
expires_at: Optional[datetime] = None
|
||||||
|
is_active: bool
|
@ -1,10 +1,9 @@
|
|||||||
from pydantic import BaseModel, EmailStr
|
from pydantic import BaseModel
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
class UserCreate(BaseModel):
|
class UserCreate(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
email: EmailStr
|
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
class UserLogin(BaseModel):
|
class UserLogin(BaseModel):
|
||||||
@ -13,7 +12,6 @@ class UserLogin(BaseModel):
|
|||||||
|
|
||||||
class UserInDB(BaseModel):
|
class UserInDB(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
email: EmailStr
|
|
||||||
hashed_password: str
|
hashed_password: str
|
||||||
uuid: str
|
uuid: str
|
||||||
skin_url: Optional[str] = None
|
skin_url: Optional[str] = None
|
||||||
@ -23,9 +21,17 @@ class UserInDB(BaseModel):
|
|||||||
total_time_played: int = 0 # Общее время игры в секундах
|
total_time_played: int = 0 # Общее время игры в секундах
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
created_at: datetime = datetime.utcnow()
|
created_at: datetime = datetime.utcnow()
|
||||||
|
code: Optional[str] = None
|
||||||
|
telegram_id: Optional[str] = None
|
||||||
|
is_verified: bool = False
|
||||||
|
code_expires_at: Optional[datetime] = None
|
||||||
class Session(BaseModel):
|
class Session(BaseModel):
|
||||||
access_token: str
|
access_token: str
|
||||||
client_token: str
|
client_token: str
|
||||||
user_uuid: str
|
user_uuid: str
|
||||||
expires_at: datetime
|
expires_at: datetime
|
||||||
|
|
||||||
|
class VerifyCode(BaseModel):
|
||||||
|
username: str
|
||||||
|
code: str
|
||||||
|
telegram_chat_id: int
|
||||||
|
@ -17,6 +17,7 @@ from cryptography.hazmat.primitives.asymmetric import padding
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import secrets
|
||||||
|
|
||||||
env_path = Path(__file__).parent.parent / ".env"
|
env_path = Path(__file__).parent.parent / ".env"
|
||||||
load_dotenv(dotenv_path=env_path)
|
load_dotenv(dotenv_path=env_path)
|
||||||
@ -37,19 +38,66 @@ class AuthService:
|
|||||||
# Сохраняем в MongoDB
|
# Сохраняем в MongoDB
|
||||||
new_user = UserInDB(
|
new_user = UserInDB(
|
||||||
username=user.username,
|
username=user.username,
|
||||||
email=user.email,
|
|
||||||
hashed_password=hashed_password,
|
hashed_password=hashed_password,
|
||||||
uuid=user_uuid,
|
uuid=user_uuid,
|
||||||
|
is_verified=False,
|
||||||
|
code=None,
|
||||||
|
code_expires_at=None
|
||||||
)
|
)
|
||||||
await users_collection.insert_one(new_user.dict())
|
await users_collection.insert_one(new_user.dict())
|
||||||
return {"status": "success", "uuid": user_uuid}
|
return {"status": "success", "uuid": user_uuid}
|
||||||
|
|
||||||
|
async def generate_code(self, username: str):
|
||||||
|
if await users_collection.find_one({"username": username}):
|
||||||
|
if await users_collection.find_one({"username": username, "is_verified": True}):
|
||||||
|
raise HTTPException(400, "User already verified")
|
||||||
|
code = secrets.token_hex(3).upper()
|
||||||
|
await users_collection.update_one({"username": username}, {"$set": {"code": code, "code_expires_at": datetime.utcnow() + timedelta(minutes=10)}})
|
||||||
|
return {"status": "success", "code": code}
|
||||||
|
else:
|
||||||
|
raise HTTPException(404, "User not found")
|
||||||
|
|
||||||
|
async def verify_code(self, username: str, code: str, telegram_chat_id: int):
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(404, "User not found")
|
||||||
|
|
||||||
|
if user["is_verified"]:
|
||||||
|
raise HTTPException(400, "User already verified")
|
||||||
|
|
||||||
|
# Проверяем код и привязку к Telegram
|
||||||
|
if user.get("telegram_chat_id") and user["telegram_chat_id"] != telegram_chat_id:
|
||||||
|
raise HTTPException(403, "This account is linked to another Telegram")
|
||||||
|
|
||||||
|
if user.get("code") != code:
|
||||||
|
raise HTTPException(400, "Invalid code")
|
||||||
|
|
||||||
|
# Обновляем chat_id при первом подтверждении
|
||||||
|
await users_collection.update_one(
|
||||||
|
{"username": username},
|
||||||
|
{"$set": {
|
||||||
|
"is_verified": True,
|
||||||
|
"telegram_chat_id": telegram_chat_id,
|
||||||
|
"code": None
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
async def get_verification_status(self, username: str):
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(404, "User not found")
|
||||||
|
return {"is_verified": user["is_verified"]}
|
||||||
|
|
||||||
async def login(self, credentials: UserLogin):
|
async def login(self, credentials: UserLogin):
|
||||||
# Ищем пользователя
|
# Ищем пользователя
|
||||||
user = await users_collection.find_one({"username": credentials.username})
|
user = await users_collection.find_one({"username": credentials.username})
|
||||||
if not user or not verify_password(credentials.password, user["hashed_password"]):
|
if not user or not verify_password(credentials.password, user["hashed_password"]):
|
||||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||||
|
|
||||||
|
if not user["is_verified"]:
|
||||||
|
raise HTTPException(status_code=401, detail="User not verified")
|
||||||
|
|
||||||
# Генерируем токены
|
# Генерируем токены
|
||||||
access_token = create_access_token({"sub": user["username"], "uuid": user["uuid"]})
|
access_token = create_access_token({"sub": user["username"], "uuid": user["uuid"]})
|
||||||
client_token = str(uuid.uuid4())
|
client_token = str(uuid.uuid4())
|
||||||
|
226
app/services/bonus.py
Normal file
226
app/services/bonus.py
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from app.db.database import db
|
||||||
|
from app.services.coins import CoinsService
|
||||||
|
from app.models.bonus import BonusType
|
||||||
|
|
||||||
|
# Коллекции для бонусов
|
||||||
|
bonus_types_collection = db.bonus_types
|
||||||
|
user_bonuses_collection = db.user_bonuses
|
||||||
|
|
||||||
|
class BonusService:
|
||||||
|
async def get_user_active_effects(self, username: str):
|
||||||
|
"""Получить активные эффекты пользователя для плагина"""
|
||||||
|
from app.db.database import users_collection
|
||||||
|
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if not user:
|
||||||
|
return {"effects": []}
|
||||||
|
|
||||||
|
# Находим активные бонусы с учетом бесконечных (expires_at = null) или действующих
|
||||||
|
active_bonuses = await user_bonuses_collection.find({
|
||||||
|
"user_id": str(user["_id"]),
|
||||||
|
"is_active": True,
|
||||||
|
}).to_list(50)
|
||||||
|
|
||||||
|
effects = []
|
||||||
|
for bonus in active_bonuses:
|
||||||
|
bonus_type = await bonus_types_collection.find_one({"id": bonus["bonus_type_id"]})
|
||||||
|
if bonus_type:
|
||||||
|
# Рассчитываем итоговое значение эффекта с учетом уровня
|
||||||
|
level = bonus.get("level", 1)
|
||||||
|
effect_value = bonus_type["base_effect_value"] + (level - 1) * bonus_type["effect_increment"]
|
||||||
|
|
||||||
|
effect = {
|
||||||
|
"effect_type": bonus_type["effect_type"],
|
||||||
|
"effect_value": effect_value
|
||||||
|
}
|
||||||
|
|
||||||
|
# Для временных бонусов добавляем срок
|
||||||
|
if bonus.get("expires_at"):
|
||||||
|
effect["expires_at"] = bonus["expires_at"].isoformat()
|
||||||
|
|
||||||
|
effects.append(effect)
|
||||||
|
|
||||||
|
return {"effects": effects}
|
||||||
|
|
||||||
|
async def list_available_bonuses(self):
|
||||||
|
"""Получить список доступных типов бонусов"""
|
||||||
|
bonuses = await bonus_types_collection.find().to_list(50)
|
||||||
|
return {"bonuses": [BonusType(**bonus) for bonus in bonuses]}
|
||||||
|
|
||||||
|
async def get_user_bonuses(self, username: str):
|
||||||
|
"""Получить активные бонусы пользователя"""
|
||||||
|
from app.db.database import users_collection
|
||||||
|
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||||
|
|
||||||
|
# Находим активные бонусы с учетом бесконечных (expires_at = null) или действующих
|
||||||
|
active_bonuses = await user_bonuses_collection.find({
|
||||||
|
"user_id": str(user["_id"]),
|
||||||
|
"is_active": True,
|
||||||
|
}).to_list(50)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for bonus in active_bonuses:
|
||||||
|
bonus_type = await bonus_types_collection.find_one({"id": bonus["bonus_type_id"]})
|
||||||
|
if bonus_type:
|
||||||
|
# Рассчитываем итоговое значение эффекта с учетом уровня
|
||||||
|
level = bonus.get("level", 1)
|
||||||
|
effect_value = bonus_type["base_effect_value"] + (level - 1) * bonus_type["effect_increment"]
|
||||||
|
|
||||||
|
bonus_data = {
|
||||||
|
"id": bonus["id"],
|
||||||
|
"name": bonus_type["name"],
|
||||||
|
"description": bonus_type["description"],
|
||||||
|
"effect_type": bonus_type["effect_type"],
|
||||||
|
"effect_value": effect_value,
|
||||||
|
"level": level,
|
||||||
|
"purchased_at": bonus["purchased_at"].isoformat(),
|
||||||
|
"can_upgrade": bonus_type["max_level"] == 0 or level < bonus_type["max_level"],
|
||||||
|
"upgrade_price": bonus_type["upgrade_price"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Для временных бонусов добавляем срок
|
||||||
|
if bonus.get("expires_at"):
|
||||||
|
bonus_data["expires_at"] = bonus["expires_at"].isoformat()
|
||||||
|
bonus_data["time_left"] = (bonus["expires_at"] - datetime.utcnow()).total_seconds()
|
||||||
|
else:
|
||||||
|
bonus_data["is_permanent"] = True
|
||||||
|
|
||||||
|
result.append(bonus_data)
|
||||||
|
|
||||||
|
return {"bonuses": result}
|
||||||
|
|
||||||
|
async def purchase_bonus(self, username: str, bonus_type_id: str):
|
||||||
|
"""Покупка базового бонуса пользователем"""
|
||||||
|
from app.db.database import users_collection
|
||||||
|
|
||||||
|
# Находим пользователя
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||||
|
|
||||||
|
# Находим тип бонуса
|
||||||
|
bonus_type = await bonus_types_collection.find_one({"id": bonus_type_id})
|
||||||
|
if not bonus_type:
|
||||||
|
raise HTTPException(status_code=404, detail="Бонус не найден")
|
||||||
|
|
||||||
|
# Проверяем, есть ли уже такой бонус у пользователя
|
||||||
|
existing_bonus = await user_bonuses_collection.find_one({
|
||||||
|
"user_id": str(user["_id"]),
|
||||||
|
"bonus_type_id": bonus_type_id,
|
||||||
|
"is_active": True
|
||||||
|
})
|
||||||
|
|
||||||
|
if existing_bonus:
|
||||||
|
raise HTTPException(status_code=400, detail="Этот бонус уже приобретен. Вы можете улучшить его.")
|
||||||
|
|
||||||
|
# Проверяем достаточно ли монет
|
||||||
|
coins_service = CoinsService()
|
||||||
|
user_coins = await coins_service.get_balance(username)
|
||||||
|
|
||||||
|
if user_coins < bonus_type["price"]:
|
||||||
|
raise HTTPException(status_code=400,
|
||||||
|
detail=f"Недостаточно монет. Требуется: {bonus_type['price']}, имеется: {user_coins}")
|
||||||
|
|
||||||
|
# Создаем запись о бонусе для пользователя
|
||||||
|
bonus_id = str(uuid.uuid4())
|
||||||
|
now = datetime.utcnow()
|
||||||
|
|
||||||
|
# Если бонус имеет длительность
|
||||||
|
expires_at = None
|
||||||
|
if bonus_type["duration"] > 0:
|
||||||
|
expires_at = now + timedelta(seconds=bonus_type["duration"])
|
||||||
|
|
||||||
|
user_bonus = {
|
||||||
|
"id": bonus_id,
|
||||||
|
"user_id": str(user["_id"]),
|
||||||
|
"username": username,
|
||||||
|
"bonus_type_id": bonus_type_id,
|
||||||
|
"level": 1, # Начальный уровень
|
||||||
|
"purchased_at": now,
|
||||||
|
"expires_at": expires_at,
|
||||||
|
"is_active": True
|
||||||
|
}
|
||||||
|
|
||||||
|
# Сохраняем бонус в БД
|
||||||
|
await user_bonuses_collection.insert_one(user_bonus)
|
||||||
|
|
||||||
|
# Списываем монеты
|
||||||
|
await coins_service.decrease_balance(username, bonus_type["price"])
|
||||||
|
|
||||||
|
# Формируем текст сообщения
|
||||||
|
duration_text = "навсегда" if bonus_type["duration"] == 0 else f"на {bonus_type['duration'] // 60} мин."
|
||||||
|
message = f"Бонус '{bonus_type['name']}' успешно приобретен {duration_text}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": message,
|
||||||
|
"remaining_coins": user_coins - bonus_type["price"]
|
||||||
|
}
|
||||||
|
|
||||||
|
async def upgrade_bonus(self, username: str, bonus_id: str):
|
||||||
|
"""Улучшение уже купленного бонуса"""
|
||||||
|
from app.db.database import users_collection
|
||||||
|
|
||||||
|
# Находим пользователя
|
||||||
|
user = await users_collection.find_one({"username": username})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||||
|
|
||||||
|
# Находим бонус пользователя
|
||||||
|
user_bonus = await user_bonuses_collection.find_one({
|
||||||
|
"id": bonus_id,
|
||||||
|
"user_id": str(user["_id"]),
|
||||||
|
"is_active": True
|
||||||
|
})
|
||||||
|
|
||||||
|
if not user_bonus:
|
||||||
|
raise HTTPException(status_code=404, detail="Бонус не найден или не принадлежит вам")
|
||||||
|
|
||||||
|
# Находим тип бонуса
|
||||||
|
bonus_type = await bonus_types_collection.find_one({"id": user_bonus["bonus_type_id"]})
|
||||||
|
if not bonus_type:
|
||||||
|
raise HTTPException(status_code=404, detail="Тип бонуса не найден")
|
||||||
|
|
||||||
|
# Проверяем ограничение на максимальный уровень
|
||||||
|
current_level = user_bonus["level"]
|
||||||
|
if bonus_type["max_level"] > 0 and current_level >= bonus_type["max_level"]:
|
||||||
|
raise HTTPException(status_code=400, detail="Достигнут максимальный уровень бонуса")
|
||||||
|
|
||||||
|
# Рассчитываем стоимость улучшения
|
||||||
|
upgrade_price = bonus_type["upgrade_price"]
|
||||||
|
|
||||||
|
# Проверяем достаточно ли монет
|
||||||
|
coins_service = CoinsService()
|
||||||
|
user_coins = await coins_service.get_balance(username)
|
||||||
|
|
||||||
|
if user_coins < upgrade_price:
|
||||||
|
raise HTTPException(status_code=400,
|
||||||
|
detail=f"Недостаточно монет. Требуется: {upgrade_price}, имеется: {user_coins}")
|
||||||
|
|
||||||
|
# Обновляем уровень бонуса
|
||||||
|
new_level = current_level + 1
|
||||||
|
|
||||||
|
await user_bonuses_collection.update_one(
|
||||||
|
{"id": bonus_id},
|
||||||
|
{"$set": {"level": new_level}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Списываем монеты
|
||||||
|
await coins_service.decrease_balance(username, upgrade_price)
|
||||||
|
|
||||||
|
# Рассчитываем новое значение эффекта
|
||||||
|
new_effect_value = bonus_type["base_effect_value"] + (new_level - 1) * bonus_type["effect_increment"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Бонус '{bonus_type['name']}' улучшен до уровня {new_level}",
|
||||||
|
"new_level": new_level,
|
||||||
|
"effect_value": new_effect_value,
|
||||||
|
"remaining_coins": user_coins - upgrade_price
|
||||||
|
}
|
@ -30,7 +30,7 @@ class CapeService:
|
|||||||
import os
|
import os
|
||||||
old_url = user["cloak_url"]
|
old_url = user["cloak_url"]
|
||||||
old_filename = os.path.basename(urlparse(old_url).path)
|
old_filename = os.path.basename(urlparse(old_url).path)
|
||||||
old_path = os.path.join("app/static/capes", old_filename)
|
old_path = os.path.join("/app/static/capes", old_filename)
|
||||||
if os.path.exists(old_path):
|
if os.path.exists(old_path):
|
||||||
try:
|
try:
|
||||||
os.remove(old_path)
|
os.remove(old_path)
|
||||||
@ -39,7 +39,7 @@ class CapeService:
|
|||||||
|
|
||||||
# Создаем папку для плащей, если ее нет
|
# Создаем папку для плащей, если ее нет
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
cape_dir = Path("app/static/capes")
|
cape_dir = Path("/app/static/capes")
|
||||||
cape_dir.mkdir(parents=True, exist_ok=True)
|
cape_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
cape_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}"
|
cape_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}"
|
||||||
|
@ -211,3 +211,68 @@ class MarketplaceService:
|
|||||||
"operation_id": operation_id,
|
"operation_id": operation_id,
|
||||||
"message": "Покупка в обработке. Предмет будет добавлен в ваш инвентарь."
|
"message": "Покупка в обработке. Предмет будет добавлен в ваш инвентарь."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def cancel_item_sale(self, username: str, item_id: str):
|
||||||
|
"""Снять предмет с продажи"""
|
||||||
|
# Находим предмет
|
||||||
|
item = await marketplace_collection.find_one({"id": item_id})
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="Предмет не найден")
|
||||||
|
|
||||||
|
# Проверяем, что пользователь является владельцем предмета
|
||||||
|
if item["seller_name"] != username:
|
||||||
|
raise HTTPException(status_code=403, detail="Вы не можете снять с продажи чужой предмет")
|
||||||
|
|
||||||
|
# Создаем операцию возврата предмета
|
||||||
|
operation_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
operation = {
|
||||||
|
"id": operation_id,
|
||||||
|
"type": "cancel_sale",
|
||||||
|
"player_name": username,
|
||||||
|
"item_id": item_id,
|
||||||
|
"item_data": item["item_data"],
|
||||||
|
"server_ip": item["server_ip"],
|
||||||
|
"status": "pending",
|
||||||
|
"created_at": datetime.utcnow()
|
||||||
|
}
|
||||||
|
|
||||||
|
await marketplace_operations.insert_one(operation)
|
||||||
|
|
||||||
|
# Удаляем предмет с торговой площадки
|
||||||
|
await marketplace_collection.delete_one({"id": item_id})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "pending",
|
||||||
|
"operation_id": operation_id,
|
||||||
|
"message": "Предмет снят с продажи и будет возвращен в ваш инвентарь"
|
||||||
|
}
|
||||||
|
|
||||||
|
async def update_item_price(self, username: str, item_id: str, new_price: int):
|
||||||
|
"""Обновить цену предмета на торговой площадке"""
|
||||||
|
# Находим предмет
|
||||||
|
item = await marketplace_collection.find_one({"id": item_id})
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="Предмет не найден")
|
||||||
|
|
||||||
|
# Проверяем, что пользователь является владельцем предмета
|
||||||
|
if item["seller_name"] != username:
|
||||||
|
raise HTTPException(status_code=403, detail="Вы не можете изменить цену чужого предмета")
|
||||||
|
|
||||||
|
# Валидация новой цены
|
||||||
|
if new_price <= 0:
|
||||||
|
raise HTTPException(status_code=400, detail="Цена должна быть положительным числом")
|
||||||
|
|
||||||
|
# Обновляем цену предмета
|
||||||
|
result = await marketplace_collection.update_one(
|
||||||
|
{"id": item_id},
|
||||||
|
{"$set": {"price": new_price}}
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.modified_count == 0:
|
||||||
|
raise HTTPException(status_code=500, detail="Не удалось обновить цену предмета")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Цена предмета обновлена на {new_price} монет"
|
||||||
|
}
|
||||||
|
@ -34,7 +34,7 @@ class SkinService:
|
|||||||
|
|
||||||
# Создаем папку для скинов, если ее нет
|
# Создаем папку для скинов, если ее нет
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
skin_dir = Path("app/static/skins")
|
skin_dir = Path("/app/static/skins")
|
||||||
skin_dir.mkdir(parents=True, exist_ok=True)
|
skin_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Генерируем имя файла
|
# Генерируем имя файла
|
||||||
|
@ -35,7 +35,7 @@ class StoreCapeService:
|
|||||||
raise HTTPException(status_code=400, detail="Файл слишком большой (максимум 2MB)")
|
raise HTTPException(status_code=400, detail="Файл слишком большой (максимум 2MB)")
|
||||||
|
|
||||||
# Создаем папку для плащей магазина, если ее нет
|
# Создаем папку для плащей магазина, если ее нет
|
||||||
cape_dir = Path("app/static/capes_store")
|
cape_dir = Path("/app/static/capes_store")
|
||||||
cape_dir.mkdir(parents=True, exist_ok=True)
|
cape_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Генерируем ID и имя файла
|
# Генерируем ID и имя файла
|
||||||
@ -124,7 +124,7 @@ class StoreCapeService:
|
|||||||
raise HTTPException(status_code=404, detail="Плащ не найден")
|
raise HTTPException(status_code=404, detail="Плащ не найден")
|
||||||
|
|
||||||
# Удаляем файл
|
# Удаляем файл
|
||||||
cape_path = Path(f"app/static/capes_store/{cape['file_name']}")
|
cape_path = Path(f"/app/static/capes_store/{cape['file_name']}")
|
||||||
if cape_path.exists():
|
if cape_path.exists():
|
||||||
try:
|
try:
|
||||||
cape_path.unlink()
|
cape_path.unlink()
|
||||||
@ -170,10 +170,10 @@ class StoreCapeService:
|
|||||||
detail=f"Недостаточно монет. Требуется: {cape['price']}, имеется: {user_coins}")
|
detail=f"Недостаточно монет. Требуется: {cape['price']}, имеется: {user_coins}")
|
||||||
|
|
||||||
# Копируем плащ из хранилища магазина в персональную папку пользователя
|
# Копируем плащ из хранилища магазина в персональную папку пользователя
|
||||||
cape_store_path = Path(f"app/static/capes_store/{cape['file_name']}")
|
cape_store_path = Path(f"/app/static/capes_store/{cape['file_name']}")
|
||||||
|
|
||||||
# Создаем папку для плащей пользователя
|
# Создаем папку для плащей пользователя
|
||||||
cape_dir = Path("app/static/capes")
|
cape_dir = Path("/app/static/capes")
|
||||||
cape_dir.mkdir(parents=True, exist_ok=True)
|
cape_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Генерируем имя файла для персонального плаща
|
# Генерируем имя файла для персонального плаща
|
||||||
|
@ -6,21 +6,32 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "3001:3000"
|
- "3001:3000"
|
||||||
|
user: "${UID:-1000}:${GID:-1000}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./app/static:/app/static
|
- ./app/static:/app/static:rw
|
||||||
environment:
|
env_file:
|
||||||
- MONGO_URI=mongodb://mongodb:27017/minecraft-api
|
- .env
|
||||||
- SECRET_KEY=your-secret-key
|
|
||||||
- ALGORITHM=HS256
|
|
||||||
- ACCESS_TOKEN_EXPIRE_MINUTES=30
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- mongodb
|
- mongodb
|
||||||
|
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"]
|
||||||
|
|
||||||
|
telegram_bot:
|
||||||
|
container_name: telegram_bot
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
user: "${UID:-1000}:${GID:-1000}"
|
||||||
|
volumes:
|
||||||
|
- ./telegram_bot.py:/app/telegram_bot.py
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
command: ["python", "telegram_bot.py"]
|
||||||
|
|
||||||
mongodb:
|
mongodb:
|
||||||
container_name: mongodb
|
container_name: mongodb
|
||||||
image: mongo:latest
|
image: mongo:latest
|
||||||
ports:
|
ports:
|
||||||
- "27017:27017"
|
- "32768:27017"
|
||||||
volumes:
|
volumes:
|
||||||
- ./mongodb:/data/db
|
- ./mongodb:/data/db
|
||||||
environment:
|
environment:
|
||||||
|
@ -8,9 +8,7 @@ RUN pip install -r requirements.txt
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
VOLUME /app/static
|
RUN mkdir -p /app/static/skins /app/static/capes /app/static/capes_store && \
|
||||||
|
chown -R 1000:1000 /app/static
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"]
|
|
||||||
|
|
3
main.py
3
main.py
@ -1,6 +1,6 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from app.api import users, skins, capes, meta, server, store, pranks, marketplace
|
from app.api import users, skins, capes, meta, server, store, pranks, marketplace, bonuses
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@ -13,6 +13,7 @@ app.include_router(server.router)
|
|||||||
app.include_router(store.router)
|
app.include_router(store.router)
|
||||||
app.include_router(pranks.router)
|
app.include_router(pranks.router)
|
||||||
app.include_router(marketplace.router)
|
app.include_router(marketplace.router)
|
||||||
|
app.include_router(bonuses.router)
|
||||||
|
|
||||||
# Монтируем статику
|
# Монтируем статику
|
||||||
app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins")
|
app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins")
|
||||||
|
@ -8,6 +8,7 @@ python-multipart>=0.0.9
|
|||||||
mongoengine>=0.24.2
|
mongoengine>=0.24.2
|
||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
pydantic>=2.0.0
|
pydantic>=2.0.0
|
||||||
pydantic[email]>=2.0.0
|
|
||||||
cryptography>=43.0.0
|
cryptography>=43.0.0
|
||||||
|
pytelegrambotapi>=2.0.0
|
||||||
|
httpx>=0.27.2
|
||||||
|
|
||||||
|
54
scripts/add_test_bonuses.py
Normal file
54
scripts/add_test_bonuses.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# scripts/add_test_bonuses.py
|
||||||
|
from app.db.database import db
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Коллекция для бонусов
|
||||||
|
bonus_types_collection = db.bonus_types
|
||||||
|
|
||||||
|
# Очищаем существующие записи
|
||||||
|
bonus_types_collection.delete_many({})
|
||||||
|
|
||||||
|
# Добавляем типы бонусов
|
||||||
|
bonus_types = [
|
||||||
|
{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"name": "Бонус опыта",
|
||||||
|
"description": "Увеличивает получаемый опыт на 100% (+10% за уровень)",
|
||||||
|
"effect_type": "experience",
|
||||||
|
"base_effect_value": 1.0, # +100%
|
||||||
|
"effect_increment": 0.1, # +10% за уровень
|
||||||
|
"price": 100,
|
||||||
|
"upgrade_price": 50,
|
||||||
|
"duration": 0, # Бесконечный
|
||||||
|
"max_level": 0 # Без ограничения уровня
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"name": "Бонус силы",
|
||||||
|
"description": "Увеличивает силу атаки на 10% (+5% за уровень)",
|
||||||
|
"effect_type": "strength",
|
||||||
|
"base_effect_value": 0.1,
|
||||||
|
"effect_increment": 0.05,
|
||||||
|
"price": 75,
|
||||||
|
"upgrade_price": 40,
|
||||||
|
"duration": 0, # Бесконечный
|
||||||
|
"max_level": 10 # Максимум 10 уровней
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"name": "Бонус скорости",
|
||||||
|
"description": "Временно увеличивает скорость передвижения на 20%",
|
||||||
|
"effect_type": "speed",
|
||||||
|
"base_effect_value": 0.2,
|
||||||
|
"effect_increment": 0.05,
|
||||||
|
"price": 40,
|
||||||
|
"upgrade_price": 30,
|
||||||
|
"duration": 1800, # 30 минут
|
||||||
|
"max_level": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Вставляем бонусы в БД
|
||||||
|
bonus_types_collection.insert_many(bonus_types)
|
||||||
|
print(f"Добавлено {len(bonus_types)} типов бонусов")
|
53
telegram_bot.py
Normal file
53
telegram_bot.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from telebot import TeleBot
|
||||||
|
import httpx
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
bot = TeleBot(os.getenv("TELEGRAM_BOT_TOKEN"))
|
||||||
|
API_URL = os.getenv("API_URL")
|
||||||
|
|
||||||
|
user_states = {} # {"chat_id": {"username": "DIKER0K"}}
|
||||||
|
|
||||||
|
@bot.message_handler(commands=['start'])
|
||||||
|
def start(message):
|
||||||
|
# Обработка deep link: /start{username}
|
||||||
|
if len(message.text.split()) > 1:
|
||||||
|
username = message.text.split()[1] # Получаем username из ссылки
|
||||||
|
user_states[message.chat.id] = {"username": username}
|
||||||
|
bot.reply_to(message, f"📋 Введите код из лаунчера:")
|
||||||
|
else:
|
||||||
|
bot.reply_to(message, "🔑 Введите ваш игровой никнейм:")
|
||||||
|
bot.register_next_step_handler(message, process_username)
|
||||||
|
|
||||||
|
def process_username(message):
|
||||||
|
user_states[message.chat.id] = {"username": message.text.strip()}
|
||||||
|
bot.reply_to(message, "📋 Теперь введите код из лаунчера:")
|
||||||
|
|
||||||
|
@bot.message_handler(func=lambda m: m.chat.id in user_states)
|
||||||
|
def verify_code(message):
|
||||||
|
username = user_states[message.chat.id]["username"]
|
||||||
|
code = message.text.strip()
|
||||||
|
print(username, code, message.chat.id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = httpx.post(
|
||||||
|
f"{API_URL}/auth/verify_code",
|
||||||
|
json={"username": username, "code": code, "telegram_chat_id": message.chat.id}, # JSON-сериализация автоматически
|
||||||
|
headers={"Content-Type": "application/json"} # Необязательно, httpx добавляет сам
|
||||||
|
)
|
||||||
|
print(response.json())
|
||||||
|
if response.status_code == 200:
|
||||||
|
bot.reply_to(message, "✅ Аккаунт подтвержден!")
|
||||||
|
else:
|
||||||
|
bot.reply_to(message, f"❌ Ошибка: {response.json().get('detail')}")
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
print(API_URL)
|
||||||
|
bot.reply_to(message, "⚠️ Сервер недоступен. Детальная информация: " + str(e))
|
||||||
|
|
||||||
|
del user_states[message.chat.id]
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
bot.polling(none_stop=True)
|
Reference in New Issue
Block a user