Compare commits

29 Commits

Author SHA1 Message Date
fa9611cc99 add:bonus store 2025-07-31 07:00:07 +05:00
8c4db146c9 fix: tabulation in marketplace update price and cancel item sale
All checks were successful
Build and Deploy / deploy (push) Successful in 22s
2025-07-21 22:28:51 +05:00
1ae08de28b add: endpoints for cancel and edit price item
All checks were successful
Build and Deploy / deploy (push) Successful in 22s
2025-07-21 22:21:39 +05:00
6e2742bc09 feat: deeplink in bot
All checks were successful
Build and Deploy / deploy (push) Successful in 21s
2025-07-21 09:47:32 +05:00
7ab955dbb4 add: logs to telegram_bot
All checks were successful
Build and Deploy / deploy (push) Successful in 21s
2025-07-21 09:37:57 +05:00
8a57fdad7a add: route get verification status
All checks were successful
Build and Deploy / deploy (push) Successful in 21s
2025-07-21 09:03:46 +05:00
a404377108 fix: build.yaml
All checks were successful
Build and Deploy / deploy (push) Successful in 12s
2025-07-21 08:11:10 +05:00
c8d8c65251 add: verify code in telegram
All checks were successful
Build and Deploy / deploy (push) Successful in 41s
2025-07-21 08:07:21 +05:00
dd71c19c6b fix!
All checks were successful
Build and Deploy / deploy (push) Successful in 12s
2025-07-21 04:12:14 +05:00
56eaaa4103 test fix :(
All checks were successful
Build and Deploy / deploy (push) Successful in 11s
2025-07-21 04:06:33 +05:00
91e54bb4e0 fix
All checks were successful
Build and Deploy / deploy (push) Successful in 11s
2025-07-21 03:57:46 +05:00
176320154f fix
All checks were successful
Build and Deploy / deploy (push) Successful in 11s
2025-07-21 03:51:56 +05:00
25b0ec0809 fix:volumes
All checks were successful
Build and Deploy / deploy (push) Successful in 12s
2025-07-21 03:45:35 +05:00
cddd20e203 fix: volumes
All checks were successful
Build and Deploy / deploy (push) Successful in 11s
2025-07-21 03:02:27 +05:00
b5369ed060 fix: conflict volume
All checks were successful
Build and Deploy / deploy (push) Successful in 12s
2025-07-21 02:50:08 +05:00
49dbc664b3 fix: skin and cape domains
All checks were successful
Build and Deploy / deploy (push) Successful in 12s
2025-07-21 02:33:06 +05:00
4bf266e2ba work version
All checks were successful
Build and Deploy / deploy (push) Successful in 11s
2025-07-21 02:06:12 +05:00
7131f6613e fix :(
All checks were successful
Build and Deploy / deploy (push) Successful in 11s
2025-07-21 01:06:51 +05:00
d2084e73ee last fix
All checks were successful
Build and Deploy / deploy (push) Successful in 11s
2025-07-21 01:00:28 +05:00
860b73554c fix
All checks were successful
Build and Deploy / deploy (push) Successful in 11s
2025-07-21 00:55:17 +05:00
ac0f58fe68 fix
All checks were successful
Build and Deploy / deploy (push) Successful in 10s
2025-07-21 00:51:26 +05:00
b505448f36 add: create .env file
All checks were successful
Build and Deploy / deploy (push) Successful in 10s
2025-07-21 00:50:14 +05:00
06ac3c01a2 fix
All checks were successful
Build and Deploy / deploy (push) Successful in 39s
2025-07-21 00:42:45 +05:00
2d377088b0 fix:action
Some checks failed
Build and Deploy / deploy (push) Failing after 4s
2025-07-21 00:41:22 +05:00
ee8bd8c052 fix: action
Some checks failed
Build and Deploy / deploy (push) Failing after 42s
2025-07-21 00:26:21 +05:00
b851a049b8 fix: actions
Some checks failed
Build and Deploy / build (push) Failing after 1m14s
2025-07-21 00:16:04 +05:00
409295358c fix: build.yaml
Some checks failed
Build and Deploy / build (push) Has been cancelled
2025-07-21 00:13:09 +05:00
2bd081fe7a add: action
Some checks failed
Build and Deploy / build (push) Has been cancelled
2025-07-21 00:09:53 +05:00
e59669f66a add: dockerfile 2025-07-20 23:24:00 +05:00
21 changed files with 685 additions and 18 deletions

View File

@ -0,0 +1,31 @@
name: Build and Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: docker
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Create .env file
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: Build and deploy
run: |
cd /home/server/popa_minecraft_launcher_api
git reset --hard HEAD
git checkout main
git pull
docker-compose down -v
docker-compose build
docker-compose up -d

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ __pycache__
.env
skins
capes
mongodb

41
app/api/bonuses.py Normal file
View 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)

View File

@ -61,3 +61,22 @@ async def submit_item_details(data: dict):
"""Получить подробные данные о предмете"""
from app.services.marketplace import MarketplaceService
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)

View File

@ -20,8 +20,8 @@ def api_root():
"homepage": "https://popa-popa.ru"
}
},
"skinDomains": ["147.78.65.214"],
"capeDomains": ["147.78.65.214"],
"skinDomains": ["147.78.65.214", "minecraft.api.popa-popa.ru"],
"capeDomains": ["147.78.65.214", "minecraft.api.popa-popa.ru"],
# Важно - возвращаем ключ как есть, без дополнительной обработки
"signaturePublickey": public_key
}

View File

@ -1,5 +1,5 @@
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.services.auth import AuthService
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}с"
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)

View File

@ -2,7 +2,8 @@ from motor.motor_asyncio import AsyncIOMotorClient
from app.core.config import MONGO_URI
client = AsyncIOMotorClient(MONGO_URI)
db = client["minecraft_auth"]
print(MONGO_URI)
db = client["minecraft-api"]
users_collection = db["users"]
sessions_collection = db["sessions"]

48
app/models/bonus.py Normal file
View 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

View File

@ -1,10 +1,9 @@
from pydantic import BaseModel, EmailStr
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
class UserLogin(BaseModel):
@ -13,7 +12,6 @@ class UserLogin(BaseModel):
class UserInDB(BaseModel):
username: str
email: EmailStr
hashed_password: str
uuid: str
skin_url: Optional[str] = None
@ -23,9 +21,17 @@ class UserInDB(BaseModel):
total_time_played: int = 0 # Общее время игры в секундах
is_active: bool = True
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):
access_token: str
client_token: str
user_uuid: str
expires_at: datetime
class VerifyCode(BaseModel):
username: str
code: str
telegram_chat_id: int

View File

@ -17,6 +17,7 @@ from cryptography.hazmat.primitives.asymmetric import padding
from dotenv import load_dotenv
import os
from pathlib import Path
import secrets
env_path = Path(__file__).parent.parent / ".env"
load_dotenv(dotenv_path=env_path)
@ -37,19 +38,66 @@ class AuthService:
# Сохраняем в MongoDB
new_user = UserInDB(
username=user.username,
email=user.email,
hashed_password=hashed_password,
uuid=user_uuid,
is_verified=False,
code=None,
code_expires_at=None
)
await users_collection.insert_one(new_user.dict())
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):
# Ищем пользователя
user = await users_collection.find_one({"username": credentials.username})
if not user or not verify_password(credentials.password, user["hashed_password"]):
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"]})
client_token = str(uuid.uuid4())

226
app/services/bonus.py Normal file
View 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
}

View File

@ -30,7 +30,7 @@ class CapeService:
import os
old_url = user["cloak_url"]
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):
try:
os.remove(old_path)
@ -39,7 +39,7 @@ class CapeService:
# Создаем папку для плащей, если ее нет
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_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}"

View File

@ -211,3 +211,68 @@ class MarketplaceService:
"operation_id": operation_id,
"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} монет"
}

View File

@ -34,7 +34,7 @@ class SkinService:
# Создаем папку для скинов, если ее нет
from pathlib import Path
skin_dir = Path("app/static/skins")
skin_dir = Path("/app/static/skins")
skin_dir.mkdir(parents=True, exist_ok=True)
# Генерируем имя файла

View File

@ -35,7 +35,7 @@ class StoreCapeService:
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)
# Генерируем ID и имя файла
@ -124,7 +124,7 @@ class StoreCapeService:
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():
try:
cape_path.unlink()
@ -170,10 +170,10 @@ class StoreCapeService:
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)
# Генерируем имя файла для персонального плаща

40
docker-compose.yml Normal file
View File

@ -0,0 +1,40 @@
services:
app:
container_name: minecraft-api
build:
context: .
dockerfile: Dockerfile
ports:
- "3001:3000"
user: "${UID:-1000}:${GID:-1000}"
volumes:
- ./app/static:/app/static:rw
env_file:
- .env
depends_on:
- 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:
container_name: mongodb
image: mongo:latest
ports:
- "32768:27017"
volumes:
- ./mongodb:/data/db
environment:
- MONGO_INITDB_ROOT_USERNAME=popa
- MONGO_INITDB_ROOT_PASSWORD=2006sit_
restart: always

14
dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
RUN mkdir -p /app/static/skins /app/static/capes /app/static/capes_store && \
chown -R 1000:1000 /app/static
EXPOSE 3000

View File

@ -1,6 +1,6 @@
from fastapi import FastAPI
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
app = FastAPI()
@ -13,6 +13,7 @@ app.include_router(server.router)
app.include_router(store.router)
app.include_router(pranks.router)
app.include_router(marketplace.router)
app.include_router(bonuses.router)
# Монтируем статику
app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins")

View File

@ -5,3 +5,10 @@ python-jose>=3.3.0
passlib>=1.7.4
bcrypt>=4.0.1
python-multipart>=0.0.9
mongoengine>=0.24.2
python-dotenv>=1.0.0
pydantic>=2.0.0
cryptography>=43.0.0
pytelegrambotapi>=2.0.0
httpx>=0.27.2

View 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
View 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)