Compare commits

..

39 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
c7f6baac5d add: new capes in store 2025-07-20 22:16:55 +05:00
75d7e29f6e fix: another player see your skin! 2025-07-20 09:12:37 +05:00
39cd14f1d7 feat: full woriking markertplace 2025-07-19 04:39:51 +05:00
6b8f116608 wokring marketplace without enchancts and durability on item 2025-07-19 04:13:04 +05:00
44e12723ad add: getting player inventory 2025-07-19 01:20:26 +05:00
259e3c373b feat: auto delete server if it is inactive for more than 5 minutes, minor fix 2025-07-18 20:34:11 +05:00
7e4e2c0bad add: store pranks 2025-07-18 18:05:45 +05:00
d52d4dbf75 add: store cape 2025-07-18 03:39:21 +05:00
ff65e4a333 required acess token and client token in set, delete skin and capes 2025-07-18 03:13:52 +05:00
2e59d03784 feat: new endpoints for users and updated models 2025-07-18 02:58:22 +05:00
44 changed files with 2788 additions and 71 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

@ -1,12 +1,32 @@
from fastapi import APIRouter, UploadFile, File
from fastapi import APIRouter, UploadFile, File, HTTPException, Form
from app.services.cape import CapeService
from app.services.auth import AuthService
router = APIRouter(tags=["Capes"])
@router.post("/user/{username}/cape")
async def set_cape(username: str, cape_file: UploadFile = File(...)):
async def set_cape(
username: str,
cape_file: UploadFile = File(...),
accessToken: str = Form(...),
clientToken: str = Form(...)
):
# Validate the token
is_valid = await AuthService().validate(accessToken, clientToken)
if not is_valid:
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
return await CapeService().set_cape(username, cape_file)
@router.delete("/user/{username}/cape")
async def remove_cape(username: str):
async def remove_cape(
username: str,
accessToken: str,
clientToken: str
):
# Validate the token
is_valid = await AuthService().validate(accessToken, clientToken)
if not is_valid:
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
return await CapeService().remove_cape(username)

82
app/api/marketplace.py Normal file
View File

@ -0,0 +1,82 @@
from fastapi import APIRouter, Query, Body
from typing import Optional
from app.models.marketplace import BuyItemRequest
router = APIRouter(
prefix="/api/marketplace",
tags=["Marketplace"]
)
@router.get("/items")
async def get_marketplace_items(
server_ip: Optional[str] = None,
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100)
):
"""Получить список предметов на торговой площадке"""
from app.services.marketplace import MarketplaceService
return await MarketplaceService().list_items(server_ip, page, limit)
@router.get("/items/{item_id}")
async def get_marketplace_item(item_id: str):
"""Получить информацию о конкретном предмете"""
from app.services.marketplace import MarketplaceService
return await MarketplaceService().get_item(item_id)
@router.post("/items/sell")
async def sell_item(
username: str = Body(...),
slot_index: int = Body(...),
amount: int = Body(...),
price: int = Body(...),
server_ip: str = Body(...)
):
"""Выставить предмет на продажу"""
from app.services.marketplace import MarketplaceService
return await MarketplaceService().add_item(username, slot_index, amount, price, server_ip)
@router.post("/items/buy/{item_id}")
async def buy_item(
item_id: str,
request: BuyItemRequest
):
"""Купить предмет"""
from app.services.marketplace import MarketplaceService
return await MarketplaceService().buy_item(request.username, item_id)
@router.get("/operations")
async def get_marketplace_operations(server_ip: str):
"""Получить список операций для выполнения на сервере"""
from app.services.marketplace import MarketplaceService
return await MarketplaceService().get_pending_operations(server_ip)
@router.post("/operations/confirm")
async def confirm_marketplace_operation(data: dict):
"""Подтвердить выполнение операции"""
from app.services.marketplace import MarketplaceService
return await MarketplaceService().confirm_operation(data["operation_id"], data.get("status"), data.get("error"))
@router.post("/items/details")
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

@ -4,15 +4,29 @@ router = APIRouter(tags=["Meta"])
@router.get("/")
def api_root():
return {
"meta": {
"serverName": "Your Auth Server",
"implementationName": "FastAPI",
"implementationVersion": "1.0.0",
"links": {
"homepage": "https://your-server.com"
# Читаем публичный ключ из файла
public_key_path = "app/keys/public_key.pem"
try:
with open(public_key_path, "r") as f:
public_key = f.read().strip()
return {
"meta": {
"serverName": "Popa Auth Server",
"implementationName": "FastAPI",
"implementationVersion": "1.0.0",
"links": {
"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
}
except Exception as e:
return {
"error": str(e),
"traceback": str(e)
}

55
app/api/pranks.py Normal file
View File

@ -0,0 +1,55 @@
from fastapi import APIRouter, HTTPException
from app.services.server.prank import PrankService
from app.models.server.prank import PrankCommandCreate, PrankCommandUpdate, PrankExecute
router = APIRouter(
prefix="/api/pranks",
tags=["Pranks"]
)
prank_service = PrankService()
@router.get("/commands")
async def get_all_prank_commands():
"""Получение всех доступных команд-пакостей"""
return await prank_service.get_all_prank_commands()
@router.get("/commands/{command_id}")
async def get_prank_command(command_id: str):
"""Получение команды-пакости по ID"""
return await prank_service.get_prank_command(command_id)
@router.post("/commands")
async def add_prank_command(command: PrankCommandCreate):
"""Добавление новой команды-пакости"""
return await prank_service.add_prank_command(command)
@router.put("/commands/{command_id}")
async def update_prank_command(command_id: str, update_data: PrankCommandUpdate):
"""Обновление команды-пакости"""
return await prank_service.update_prank_command(command_id, update_data)
@router.delete("/commands/{command_id}")
async def delete_prank_command(command_id: str):
"""Удаление команды-пакости"""
return await prank_service.delete_prank_command(command_id)
@router.get("/servers")
async def get_all_servers():
"""Получение списка всех доступных серверов"""
return await prank_service.get_all_servers()
@router.get("/servers/{server_id}/players")
async def get_server_online_players(server_id: str):
"""Получение списка онлайн игроков на сервере"""
return await prank_service.get_server_online_players(server_id)
@router.post("/execute")
async def execute_prank(username: str, prank_data: PrankExecute):
"""Выполнение пакости (списание монет и выполнение команды)"""
return await prank_service.execute_prank(
username,
prank_data.command_id,
prank_data.target_player,
prank_data.server_id
)

60
app/api/server.py Normal file
View File

@ -0,0 +1,60 @@
from fastapi import APIRouter
from app.services.server.command import CommandService
from app.services.server.event import EventService
from app.models.server.command import ServerCommand, InventoryRequest
from datetime import datetime
import uuid
router = APIRouter(
prefix="/api/server",
tags=["Server Management"]
)
@router.post("/events")
async def receive_server_event(event_data: dict):
# Обновляем активность сервера
server_ip = event_data.get("server_ip")
if server_ip:
await update_server_activity(server_ip)
return await EventService().process_event(event_data)
@router.post("/commands")
async def add_server_command(command_data: ServerCommand):
# Обновляем last_activity для сервера
await CommandService()._update_server_activity(command_data.server_ip)
return await CommandService().add_command(command_data)
@router.get("/commands")
async def get_server_commands(server_ip: str):
return await CommandService().get_commands(server_ip)
@router.post("/inventory")
async def request_player_inventory(inventory_request: InventoryRequest):
"""Создаёт запрос на получение инвентаря игрока"""
return await CommandService().request_inventory(inventory_request)
@router.get("/inventory/requests")
async def get_inventory_requests(server_ip: str):
"""Получает список запросов инвентаря для сервера"""
return await CommandService().get_inventory_requests(server_ip)
@router.post("/inventory/submit")
async def submit_inventory(inventory_data: dict):
"""Принимает данные инвентаря от сервера"""
return await CommandService().submit_inventory(inventory_data)
@router.get("/inventory/{request_id}")
async def get_inventory_result(request_id: str):
"""Получает результаты запроса инвентаря"""
return await CommandService().get_inventory_result(request_id)
async def update_server_activity(server_ip):
"""Обновляет время последней активности сервера"""
from app.db.database import db
game_servers_collection = db.game_servers
await game_servers_collection.update_one(
{"ip": server_ip},
{"$set": {"last_activity": datetime.utcnow()}},
upsert=False
)

View File

@ -1,12 +1,33 @@
from fastapi import APIRouter, UploadFile, File, Form
from fastapi import APIRouter, UploadFile, File, Form, HTTPException
from app.services.skin import SkinService
from app.services.auth import AuthService
router = APIRouter(tags=["Skins"])
@router.post("/user/{username}/skin")
async def set_skin(username: str, skin_file: UploadFile = File(...), skin_model: str = Form("classic")):
async def set_skin(
username: str,
skin_file: UploadFile = File(...),
skin_model: str = Form("classic"),
accessToken: str = Form(...),
clientToken: str = Form(...)
):
# Validate the token
is_valid = await AuthService().validate(accessToken, clientToken)
if not is_valid:
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
return await SkinService().set_skin(username, skin_file, skin_model)
@router.delete("/user/{username}/skin")
async def remove_skin(username: str):
async def remove_skin(
username: str,
accessToken: str,
clientToken: str
):
# Validate the token
is_valid = await AuthService().validate(accessToken, clientToken)
if not is_valid:
raise HTTPException(status_code=401, detail="Invalid authentication tokens")
return await SkinService().remove_skin(username)

61
app/api/store.py Normal file
View File

@ -0,0 +1,61 @@
from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException
from app.services.store_cape import StoreCapeService
from app.models.cape import CapeStoreUpdate, CapePurchase
from typing import Optional
router = APIRouter(
prefix="/store",
tags=["Store"]
)
store_cape_service = StoreCapeService()
@router.get("/capes")
async def get_all_capes():
"""Получение списка всех плащей в магазине"""
return await store_cape_service.get_all_capes()
@router.get("/capes/{cape_id}")
async def get_cape_by_id(cape_id: str):
"""Получение плаща по ID"""
return await store_cape_service.get_cape_by_id(cape_id)
@router.post("/capes")
async def add_cape(
name: str = Form(...),
description: str = Form(...),
price: int = Form(...),
cape_file: UploadFile = File(...)
):
"""Добавление нового плаща в магазин"""
return await store_cape_service.add_cape(name, description, price, cape_file)
@router.put("/capes/{cape_id}")
async def update_cape(cape_id: str, update_data: CapeStoreUpdate):
"""Обновление информации о плаще"""
return await store_cape_service.update_cape(cape_id, update_data)
@router.delete("/capes/{cape_id}")
async def delete_cape(cape_id: str):
"""Удаление плаща из магазина"""
return await store_cape_service.delete_cape(cape_id)
@router.post("/purchase/cape")
async def purchase_cape(username: str, cape_id: str):
"""Покупка плаща пользователем"""
return await store_cape_service.purchase_cape(username, cape_id)
@router.get("/user/{username}/capes")
async def get_user_purchased_capes(username: str):
"""Получение всех приобретенных плащей пользователя"""
return await store_cape_service.get_user_purchased_capes(username)
@router.post("/user/{username}/capes/activate/{cape_id}")
async def activate_purchased_cape(username: str, cape_id: str):
"""Активация приобретенного плаща"""
return await store_cape_service.activate_purchased_cape(username, cape_id)
@router.post("/user/{username}/capes/deactivate/{cape_id}")
async def deactivate_purchased_cape(username: str, cape_id: str):
"""Деактивация приобретенного плаща"""
return await store_cape_service.deactivate_purchased_cape(username, cape_id)

View File

@ -1,7 +1,17 @@
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
from datetime import datetime
import json
from fastapi import HTTPException
from datetime import datetime, timedelta
from app.models.server.event import PlayerEvent, OnlinePlayersUpdate
from app.models.server.playtime import PlayerSession, PlayerPlaytime
from app.services.coins import CoinsService
coins_service = CoinsService()
router = APIRouter(
tags=["Users"]
@ -45,3 +55,77 @@ async def join_server(request_data: dict = Body(...)):
@router.get("/sessionserver/session/minecraft/hasJoined")
async def has_joined(username: str, serverId: str):
return await AuthService().has_joined(username, serverId)
@router.get("/users/{username}/coins")
async def get_user_coins(username: str):
coins_data = await coins_service.get_player_coins(username)
if not coins_data:
raise HTTPException(status_code=404, detail="User not found")
return coins_data
@router.get("/users")
async def get_users():
"""Получение списка всех пользователей"""
users = await users_collection.find().to_list(1000)
# Исключаем чувствительные данные перед отправкой
safe_users = []
for user in users:
safe_users.append({
"username": user["username"],
"uuid": user["uuid"],
"skin_url": user.get("skin_url"),
"cloak_url": user.get("cloak_url"),
"coins": user.get("coins", 0),
"total_time_played": user.get("total_time_played", 0),
"is_active": user.get("is_active", True)
})
return {"users": safe_users, "count": len(safe_users)}
@router.get("/users/{uuid}")
async def get_user_by_uuid(uuid: str):
"""Получение пользователя по UUID"""
user = await users_collection.find_one({"uuid": uuid})
if not user:
# Пробуем разные форматы UUID
if '-' in uuid:
user = await users_collection.find_one({"uuid": uuid.replace('-', '')})
else:
formatted_uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}"
user = await users_collection.find_one({"uuid": formatted_uuid})
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Исключаем чувствительные данные
safe_user = {
"username": user["username"],
"uuid": user["uuid"],
"skin_url": user.get("skin_url"),
"cloak_url": user.get("cloak_url"),
"coins": user.get("coins", 0),
"total_time_played": user.get("total_time_played", 0),
"is_active": user.get("is_active", True),
"created_at": user.get("created_at")
}
if "total_time_played" in safe_user:
total_time = safe_user["total_time_played"]
hours, remainder = divmod(total_time, 3600)
minutes, seconds = divmod(remainder, 60)
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,4 +1,25 @@
from pydantic import BaseModel
from typing import Optional
class CapeUpdate(BaseModel):
cape_url: str
class CapeStore(BaseModel):
id: str
name: str
description: str
price: int
file_name: str
class CapeStoreCreate(BaseModel):
name: str
description: str
price: int
class CapeStoreUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
price: Optional[int] = None
class CapePurchase(BaseModel):
cape_id: str

20
app/models/marketplace.py Normal file
View File

@ -0,0 +1,20 @@
from pydantic import BaseModel
from typing import Optional, List, Dict, Any
class MarketplaceItemBase(BaseModel):
material: str
amount: int
price: int
seller_name: str
server_ip: str
display_name: Optional[str] = None
lore: Optional[List[str]] = None
enchants: Optional[Dict[str, int]] = None
item_data: Optional[Dict[str, Any]] = None # Дополнительные данные предмета
class MarketplaceItem(MarketplaceItemBase):
id: str
created_at: str
class BuyItemRequest(BaseModel):
username: str

View File

@ -0,0 +1,13 @@
from pydantic import BaseModel
from typing import Optional
class ServerCommand(BaseModel):
command: str
server_ip: str
require_online_player: Optional[bool] = False
target_message: Optional[str] = None # Сообщение для цели
global_message: Optional[str] = None # Сообщение для остальных
class InventoryRequest(BaseModel):
server_ip: str
player_name: str

View File

@ -0,0 +1,17 @@
from pydantic import BaseModel
from typing import Optional, List, Dict
from datetime import datetime
class PlayerEvent(BaseModel):
event_type: str
player_id: Optional[str] = None
player_name: str
duration: Optional[int] = None # в секундах
timestamp: Optional[int] = None # UNIX timestamp в миллисекундах
server_ip: str
class OnlinePlayersUpdate(BaseModel):
event_type: str = "online_players_update"
players: List[Dict]
timestamp: int
server_ip: str

View File

@ -0,0 +1,17 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
class PlayerSession(BaseModel):
player_id: str
player_name: str
server_ip: str
start_time: datetime
end_time: Optional[datetime] = None
duration: Optional[int] = None # в секундах
class PlayerPlaytime(BaseModel):
player_id: str
player_name: str
total_time: int # общее время в секундах
last_coins_update: datetime # последнее время начисления монет

View File

@ -0,0 +1,36 @@
from pydantic import BaseModel, Field
from typing import Optional, List
class PrankCommandCreate(BaseModel):
name: str
description: str
price: int
command_template: str
server_ids: List[str] = Field(
default=[],
description='Список серверов, где доступна команда. Использование ["*"] означает доступность на всех серверах'
)
targetDescription: Optional[str] = None # Сообщение для целевого игрока
globalDescription: Optional[str] = None # Сообщение для всех остальных
class PrankCommandUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
price: Optional[int] = None
command_template: Optional[str] = None
server_ids: Optional[List[str]] = None
targetDescription: Optional[str] = None
globalDescription: Optional[str] = None
class PrankCommand(BaseModel):
id: str
name: str
description: str
price: int
command_template: str
server_ids: List[str] = []
class PrankExecute(BaseModel):
command_id: str
target_player: str
server_id: str

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,17 +12,26 @@ class UserLogin(BaseModel):
class UserInDB(BaseModel):
username: str
email: EmailStr
hashed_password: str
uuid: str
skin_url: Optional[str] = None
skin_model: Optional[str] = "classic"
cloak_url: Optional[str] = None
coins: int = 0 # Новое поле для монет
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())
@ -108,7 +156,7 @@ class AuthService:
textures = {
"timestamp": int(datetime.now().timestamp() * 1000),
"profileId": user["uuid"], # UUID с дефисами
"profileId": user["uuid"].replace("-", ""),
"profileName": user["username"],
"textures": {}
}
@ -125,28 +173,66 @@ class AuthService:
textures_json = json.dumps(textures).encode()
base64_textures = base64.b64encode(textures_json).decode()
# Подписываем текстуры
with open("private_key.pem", "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None
try:
# Подписываем текстуры
private_key_path = "app/keys/private_key.pem"
with open(private_key_path, "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None
)
signature = private_key.sign(
base64.b64encode(textures_json),
padding.PKCS1v15(),
hashes.SHA1()
)
signature = private_key.sign(
textures_json,
padding.PKCS1v15(),
hashes.SHA1()
)
signature_base64 = base64.b64encode(signature).decode()
return JSONResponse({
"id": user["uuid"].replace("-", ""), # Уберите дефисы
"name": user["username"],
"properties": [{
"name": "textures",
"value": base64_textures,
"signature": base64.b64encode(signature).decode()
}]
})
return {
"id": user["uuid"].replace("-", ""),
"name": user["username"],
"properties": [{
"name": "textures",
"value": base64_textures,
"signature": signature_base64
}]
}
except Exception as e:
print(f"Error signing textures: {e}")
# В случае ошибки возвращаем текстуры без подписи
return {
"id": user["uuid"].replace("-", ""),
"name": user["username"],
"properties": [{
"name": "textures",
"value": base64_textures
}]
}
# # Подписываем текстуры
# with open("private_key.pem", "rb") as key_file:
# private_key = serialization.load_pem_private_key(
# key_file.read(),
# password=None
# )
# signature = private_key.sign(
# textures_json,
# padding.PKCS1v15(),
# hashes.SHA1()
# )
# return JSONResponse({
# "id": user["uuid"].replace("-", ""), # Уберите дефисы
# "name": user["username"],
# "properties": [{
# "name": "textures",
# "value": base64_textures,
# # "signature": base64.b64encode(signature).decode()
# }]
# })
async def join_server(self, request_data: dict):
access_token = request_data.get("accessToken")
@ -186,24 +272,59 @@ class AuthService:
if not session:
raise HTTPException(status_code=403, detail="Not joined this server")
textures = {}
if user.get("skin_url"):
textures["SKIN"] = {"url": user["skin_url"]}
if user.get("cloak_url"):
textures["CAPE"] = {"url": user["cloak_url"]}
textures_value = base64.b64encode(json.dumps({
"timestamp": int(datetime.now().timestamp()),
"profileId": user["uuid"].replace("-", ""), # UUID без дефисов
"profileName": username,
"textures": textures
}).encode()).decode()
return {
"id": user["uuid"].replace("-", ""), # UUID без дефисов
"name": username,
"properties": [{
"name": "textures",
"value": textures_value
}] if textures else []
textures = {
"timestamp": int(datetime.now().timestamp() * 1000),
"profileId": user["uuid"].replace("-", ""),
"profileName": user["username"],
"textures": {}
}
if user.get("skin_url"):
textures["textures"]["SKIN"] = {
"url": user["skin_url"],
"metadata": {"model": user.get("skin_model", "classic")}
}
if user.get("cloak_url"):
textures["textures"]["CAPE"] = {"url": user["cloak_url"]}
textures_json = json.dumps(textures).encode()
base64_textures = base64.b64encode(textures_json).decode()
try:
# Подписываем текстуры
private_key_path = "app/keys/private_key.pem"
with open(private_key_path, "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None
)
signature = private_key.sign(
base64.b64encode(textures_json),
padding.PKCS1v15(),
hashes.SHA1()
)
signature_base64 = base64.b64encode(signature).decode()
return {
"id": user["uuid"].replace("-", ""),
"name": user["username"],
"properties": [{
"name": "textures",
"value": base64_textures,
"signature": signature_base64
}]
}
except Exception as e:
print(f"Error signing textures: {e}")
# В случае ошибки возвращаем текстуры без подписи
return {
"id": user["uuid"].replace("-", ""),
"name": user["username"],
"properties": [{
"name": "textures",
"value": base64_textures
}]
}

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}"

142
app/services/coins.py Normal file
View File

@ -0,0 +1,142 @@
from datetime import datetime
from app.db.database import users_collection, sessions_collection
from fastapi import HTTPException
class CoinsService:
async def update_player_coins(self, player_id: str, player_name: str, online_time: int, server_ip: str):
"""Обновляет монеты игрока на основе времени онлайн"""
# Находим пользователя
user = await self._find_user_by_uuid(player_id)
if not user:
return # Пользователь не найден
# Находим последнее обновление монет
last_update = await sessions_collection.find_one({
"player_id": player_id,
"server_ip": server_ip,
"update_type": "coins_update"
}, sort=[("timestamp", -1)])
now = datetime.now()
current_coins = user.get("coins", 0)
current_total_time = user.get("total_time_played", 0)
if last_update:
# Время с последнего начисления
last_timestamp = last_update["timestamp"]
seconds_since_update = int((now - last_timestamp).total_seconds())
# Начисляем монеты только за полные минуты
minutes_to_reward = seconds_since_update // 60
# Если прошло меньше минуты, пропускаем
if minutes_to_reward < 1:
return
else:
# Первое обновление (ограничиваем для безопасности)
minutes_to_reward = min(online_time // 60, 5)
if minutes_to_reward > 0:
# Обновляем монеты и время
new_coins = current_coins + minutes_to_reward
new_total_time = current_total_time + (minutes_to_reward * 60)
# Сохраняем в БД
await users_collection.update_one(
{"_id": user["_id"]},
{"$set": {
"coins": new_coins,
"total_time_played": new_total_time
}}
)
# Сохраняем запись о начислении
await sessions_collection.insert_one({
"player_id": player_id,
"player_name": player_name,
"server_ip": server_ip,
"update_type": "coins_update",
"timestamp": now,
"minutes_added": minutes_to_reward,
"coins_added": minutes_to_reward
})
print(f"[{now}] Игроку {user.get('username')} начислено {minutes_to_reward} монет. "
f"Всего монет: {new_coins}")
async def _find_user_by_uuid(self, player_id: str):
"""Находит пользователя по UUID с поддержкой разных форматов"""
# Пробуем найти как есть
user = await users_collection.find_one({"uuid": player_id})
if user:
return user
# Пробуем разные форматы UUID
if '-' in player_id:
user = await users_collection.find_one({"uuid": player_id.replace('-', '')})
else:
formatted_uuid = f"{player_id[:8]}-{player_id[8:12]}-{player_id[12:16]}-{player_id[16:20]}-{player_id[20:]}"
user = await users_collection.find_one({"uuid": formatted_uuid})
return user
async def get_player_coins(self, username: str):
"""Возвращает информацию о монетах и времени игрока"""
user = await users_collection.find_one({"username": username})
if not user:
return None
total_time = user.get("total_time_played", 0)
hours, remainder = divmod(total_time, 3600)
minutes, seconds = divmod(remainder, 60)
return {
"username": username,
"coins": user.get("coins", 0),
"total_time_played": {
"seconds": total_time,
"formatted": f"{hours}ч {minutes}м {seconds}с"
}
}
async def get_balance(self, username: str) -> int:
"""Получить текущий баланс пользователя"""
user = await users_collection.find_one({"username": username})
if not user:
raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден")
return user.get("coins", 0)
async def increase_balance(self, username: str, amount: int) -> int:
"""Увеличить баланс пользователя"""
if amount <= 0:
raise ValueError("Сумма должна быть положительной")
result = await users_collection.update_one(
{"username": username},
{"$inc": {"coins": amount}}
)
if result.modified_count == 0:
raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден")
user = await users_collection.find_one({"username": username})
return user.get("coins", 0)
async def decrease_balance(self, username: str, amount: int) -> int:
"""Уменьшить баланс пользователя"""
if amount <= 0:
raise ValueError("Сумма должна быть положительной")
result = await users_collection.update_one(
{"username": username},
{"$inc": {"coins": -amount}} # Уменьшаем на отрицательное значение
)
if result.modified_count == 0:
raise HTTPException(status_code=404, detail=f"Пользователь {username} не найден")
user = await users_collection.find_one({"username": username})
return user.get("coins", 0)

278
app/services/marketplace.py Normal file
View File

@ -0,0 +1,278 @@
import uuid
from datetime import datetime
from fastapi import HTTPException
from app.db.database import db
from app.services.coins import CoinsService
from app.services.server.command import CommandService
# Коллекция для хранения товаров на торговой площадке
marketplace_collection = db.marketplace_items
# Добавьте новую коллекцию для операций
marketplace_operations = db.marketplace_operations
# Добавьте эту функцию
def _serialize_mongodb_doc(doc):
"""Преобразует MongoDB документ для JSON сериализации"""
if doc is None:
return None
# Добавить проверку на список
if isinstance(doc, list):
return [_serialize_mongodb_doc(item) for item in doc]
result = {}
for key, value in doc.items():
# Обработка ObjectId
if key == "_id":
result["_id"] = str(value)
continue
# Обработка ISODate
if isinstance(value, datetime):
result[key] = value.isoformat()
# Обработка вложенных словарей
elif isinstance(value, dict):
if "$date" in value:
# Это ISODate
result[key] = datetime.fromisoformat(value["$date"].replace("Z", "+00:00")).isoformat()
else:
result[key] = _serialize_mongodb_doc(value)
# Обработка списков
elif isinstance(value, list):
result[key] = [_serialize_mongodb_doc(item) if isinstance(item, dict) else item for item in value]
else:
result[key] = value
return result
class MarketplaceService:
async def list_items(self, server_ip: str = None, page: int = 1, limit: int = 20):
"""Получить список предметов на торговой площадке"""
query = {}
if server_ip:
query["server_ip"] = server_ip
total = await marketplace_collection.count_documents(query)
items_cursor = marketplace_collection.find(query) \
.sort("created_at", -1) \
.skip((page - 1) * limit) \
.limit(limit)
items = await items_cursor.to_list(limit)
# Преобразуем каждый документ
serialized_items = [_serialize_mongodb_doc(item) for item in items]
return {
"items": serialized_items,
"total": total,
"page": page,
"pages": (total + limit - 1) // limit
}
async def get_item(self, item_id: str):
"""Получить информацию о конкретном предмете"""
item = await marketplace_collection.find_one({"id": item_id})
if not item:
raise HTTPException(status_code=404, detail="Предмет не найден")
return _serialize_mongodb_doc(item)
async def add_item(self, username: str, slot_index: int, amount: int, price: int, server_ip: str):
"""Выставить предмет на продажу"""
# Создаем операцию продажи
operation_id = str(uuid.uuid4())
operation = {
"id": operation_id,
"type": "sell",
"player_name": username,
"slot_index": slot_index,
"amount": amount,
"price": price,
"server_ip": server_ip,
"status": "pending",
"created_at": datetime.utcnow()
}
await marketplace_operations.insert_one(operation)
return {"status": "pending", "operation_id": operation_id}
async def get_pending_operations(self, server_ip: str):
"""Получить список операций для выполнения на сервере"""
operations = await marketplace_operations.find({
"server_ip": server_ip,
"status": "pending"
}).to_list(100)
return {
"operations": _serialize_mongodb_doc(operations)
}
async def confirm_operation(self, operation_id: str, status: str = "success", error: str = None):
"""Подтвердить выполнение операции"""
update = {
"status": status
}
if error:
update["error"] = error
result = await marketplace_operations.update_one(
{"id": operation_id},
{"$set": update}
)
return {"status": "success"}
async def update_item_details(self, operation_id: str, item_data: dict):
"""Обновить детальную информацию о предмете"""
operation = await marketplace_operations.find_one({"id": operation_id})
if not operation:
return {"status": "error", "message": "Операция не найдена"}
# Создаем запись о предмете на торговой площадке
item_id = str(uuid.uuid4())
marketplace_item = {
"id": item_id,
"material": item_data.get("material"),
"amount": item_data.get("amount"),
"price": operation.get("price"),
"seller_name": operation.get("player_name"),
"server_ip": operation.get("server_ip"),
"display_name": item_data.get("meta", {}).get("display_name"),
"lore": item_data.get("meta", {}).get("lore"),
"enchants": item_data.get("meta", {}).get("enchants"),
"durability": item_data.get("meta", {}).get("durability"),
"item_data": item_data,
"created_at": datetime.utcnow()
}
await marketplace_collection.insert_one(marketplace_item)
# Обновляем операцию
await marketplace_operations.update_one(
{"id": operation_id},
{"$set": {"item_id": item_id, "status": "completed"}}
)
return {"status": "success"}
async def buy_item(self, buyer_username: str, item_id: str):
"""Купить предмет с торговой площадки"""
# 1. Находим предмет
item = await marketplace_collection.find_one({"id": item_id})
if not item:
raise HTTPException(status_code=404, detail="Предмет не найден")
# 2. Проверяем, что покупатель не является продавцом
if item["seller_name"] == buyer_username:
raise HTTPException(status_code=400, detail="Вы не можете купить свой же предмет")
# 3. Проверяем баланс покупателя
coins_service = CoinsService()
buyer_balance = await coins_service.get_balance(buyer_username)
if buyer_balance < item["price"]:
raise HTTPException(status_code=400,
detail=f"Недостаточно монет. Требуется: {item['price']}, имеется: {buyer_balance}")
# 4. Создаем операцию покупки
operation_id = str(uuid.uuid4())
operation = {
"id": operation_id,
"type": "buy",
"player_name": buyer_username,
"item_id": item_id,
"item_data": item["item_data"],
"price": item["price"],
"server_ip": item["server_ip"],
"status": "pending",
"created_at": datetime.utcnow()
}
await marketplace_operations.insert_one(operation)
# 5. Списываем деньги с покупателя
await coins_service.decrease_balance(buyer_username, item["price"])
# 6. Начисляем деньги продавцу
await coins_service.increase_balance(item["seller_name"], item["price"])
# 7. Удаляем предмет с торговой площадки
await marketplace_collection.delete_one({"id": item_id})
return {
"status": "pending",
"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

@ -0,0 +1,234 @@
import uuid
from datetime import datetime
from fastapi import HTTPException
from typing import Dict
from app.db.database import db
import asyncio
# Создаем коллекции для хранения команд и инвентаря
pending_commands_collection = db.pending_commands
inventory_requests_collection = db.inventory_requests
inventory_collection = db.inventory
game_servers_collection = db.game_servers
class CommandService:
async def add_command(self, command_data):
try:
command_id = str(uuid.uuid4())
command_doc = {
"id": command_id,
"command": command_data.command,
"server_ip": command_data.server_ip,
"require_online_player": command_data.require_online_player,
"target_message": command_data.target_message if hasattr(command_data, 'target_message') else None,
"global_message": command_data.global_message if hasattr(command_data, 'global_message') else None,
"created_at": datetime.utcnow()
}
await pending_commands_collection.insert_one(command_doc)
print(f"[{datetime.now()}] Добавлена команда: {command_data.command} "
f"для сервера {command_data.server_ip}")
# Обновляем last_activity для сервера
await self._update_server_activity(command_data.server_ip)
return {"status": "success", "command_id": command_id}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
async def get_commands(self, server_ip: str):
try:
# Получаем команды для указанного сервера
commands_cursor = pending_commands_collection.find({"server_ip": server_ip})
commands = await commands_cursor.to_list(1000)
result_commands = [
{
"id": cmd["id"],
"command": cmd["command"],
"require_online_player": cmd["require_online_player"],
"target_message": cmd.get("target_message"),
"global_message": cmd.get("global_message")
}
for cmd in commands
]
# Удаляем полученные команды (чтобы не выполнять их повторно)
await pending_commands_collection.delete_many({"server_ip": server_ip})
return {"status": "success", "commands": result_commands}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
async def request_inventory(self, inventory_request):
"""Создаёт запрос на получение инвентаря игрока"""
try:
request_id = str(uuid.uuid4())
inventory_request_doc = {
"id": request_id,
"server_ip": inventory_request.server_ip,
"player_name": inventory_request.player_name,
"created_at": datetime.utcnow(),
"status": "pending"
}
await inventory_requests_collection.insert_one(inventory_request_doc)
print(f"[{datetime.now()}] Запрос инвентаря игрока {inventory_request.player_name} "
f"с сервера {inventory_request.server_ip}")
# Обновляем last_activity для сервера
await self._update_server_activity(inventory_request.server_ip)
return {"status": "pending", "request_id": request_id}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
async def get_inventory_requests(self, server_ip: str):
"""Получает запросы на инвентарь для указанного сервера"""
try:
requests_cursor = inventory_requests_collection.find(
{"server_ip": server_ip, "status": "pending"}
)
requests = await requests_cursor.to_list(1000)
result_requests = [
{
"id": req["id"],
"player_name": req["player_name"]
}
for req in requests
]
# Помечаем запросы как обработанные
for req in result_requests:
await inventory_requests_collection.update_one(
{"id": req["id"]},
{"$set": {"status": "processing"}}
)
return {"status": "success", "inventory_requests": result_requests}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
async def submit_inventory(self, inventory_data: dict):
"""Принимает данные инвентаря от сервера"""
try:
request_id = inventory_data.get("request_id")
request = await inventory_requests_collection.find_one({"id": request_id})
if not request:
raise HTTPException(status_code=404, detail="Запрос не найден")
player_name = request["player_name"]
server_ip = request["server_ip"]
# Обновляем или создаем запись инвентаря
await inventory_collection.update_one(
{
"player_name": player_name,
"server_ip": server_ip
},
{
"$set": {
"inventory_data": inventory_data.get("inventory", []),
"updated_at": datetime.utcnow()
}
},
upsert=True # Создает новую запись, если не найдена существующая
)
# Помечаем запрос как выполненный
await inventory_requests_collection.update_one(
{"id": request_id},
{"$set": {"status": "completed"}}
)
return {"status": "success"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
async def get_inventory_result(self, request_id: str):
"""Получает результаты запроса инвентаря"""
request = await inventory_requests_collection.find_one({"id": request_id})
if not request:
raise HTTPException(status_code=404, detail="Запрос не найден")
if request["status"] != "completed":
return {"status": request["status"]}
# Получаем инвентарь из коллекции inventory
inventory = await inventory_collection.find_one({
"player_name": request["player_name"],
"server_ip": request["server_ip"]
})
if not inventory:
raise HTTPException(status_code=404, detail="Инвентарь не найден")
return {
"status": "completed",
"result": {
"player_name": inventory["player_name"],
"server_ip": inventory["server_ip"],
"inventory_data": inventory["inventory_data"],
"updated_at": inventory["updated_at"]
}
}
async def get_player_inventory(self, player_name: str, server_ip: str, timeout: int = 10):
"""Запрашивает и ждет получения инвентаря игрока"""
try:
# Проверяем, есть ли уже актуальный инвентарь
existing_inventory = await inventory_collection.find_one({
"player_name": player_name,
"server_ip": server_ip
})
# Если инвентарь уже есть и он достаточно свежий (не старше 1 минуты)
if existing_inventory and "updated_at" in existing_inventory:
if (datetime.utcnow() - existing_inventory["updated_at"]).total_seconds() < 60:
return {
"status": "success",
"player_name": existing_inventory["player_name"],
"server_ip": existing_inventory["server_ip"],
"inventory": existing_inventory["inventory_data"],
"updated_at": existing_inventory["updated_at"]
}
# Запрашиваем новый инвентарь
request_id = str(uuid.uuid4())
inventory_request_doc = {
"id": request_id,
"server_ip": server_ip,
"player_name": player_name,
"created_at": datetime.utcnow(),
"status": "pending"
}
await inventory_requests_collection.insert_one(inventory_request_doc)
print(f"[{datetime.now()}] Запрос инвентаря игрока {player_name} "
f"с сервера {server_ip}")
# Обновляем last_activity для сервера
await self._update_server_activity(server_ip)
# Ждем ответа от сервера
start_time = datetime.utcnow()
while (datetime.utcnow() - start_time).total_seconds() < timeout:
result = await self.get_inventory_result(request_id)
if result["status"] == "completed":
return result
await asyncio.sleep(1) # Ждем 1 секунду перед следующей проверкой
raise HTTPException(status_code=504, detail="Timeout waiting for inventory")
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
async def _update_server_activity(self, server_ip):
"""Обновляет время последней активности для сервера"""
await game_servers_collection.update_one(
{"ip": server_ip},
{"$set": {"last_activity": datetime.utcnow()}},
upsert=False # Не создаем новый сервер, только обновляем существующий
)

View File

@ -0,0 +1,290 @@
from fastapi import HTTPException
from datetime import datetime
import json
from app.services.coins import CoinsService
from app.models.server.event import PlayerEvent, OnlinePlayersUpdate
import uuid
class EventService:
def __init__(self):
self.coins_service = CoinsService()
async def process_event(self, event_data):
"""Обработка событий от сервера Minecraft"""
try:
# Проверяем формат ваших событий (event_type вместо type)
event_type = event_data.get("event_type")
if not event_type:
# Для совместимости со старым форматом
event_type = event_data.get("type")
if not event_type:
raise HTTPException(status_code=400, detail="Missing event type")
server_ip = event_data.get("server_ip")
if not server_ip:
raise HTTPException(status_code=400, detail="Missing server IP")
# Преобразуем ваши типы событий в нужные форматы
if event_type == "online_players_update":
# Регистрируем сервер, если его нет
await self._register_server(server_ip, event_data)
# Обновляем данные об онлайн игроках
players = event_data.get("players", [])
await self._update_online_players(server_ip, players)
return {"status": "success"}
elif event_type == "player_join":
player_id = event_data.get("player_id")
player_name = event_data.get("player_name")
if not player_id or not player_name:
raise HTTPException(status_code=400, detail="Missing player data")
# Регистрируем вход игрока
await self._register_player_login(server_ip, player_id, player_name)
return {"status": "success"}
elif event_type == "player_quit":
player_id = event_data.get("player_id")
player_name = event_data.get("player_name")
if not player_id or not player_name:
raise HTTPException(status_code=400, detail="Missing player data")
# Регистрируем выход игрока
await self._register_player_logout(server_ip, player_id, player_name)
return {"status": "success"}
elif event_type == "player_session":
player_id = event_data.get("player_id")
player_name = event_data.get("player_name")
duration = event_data.get("duration", 0)
if not player_id or not player_name:
raise HTTPException(status_code=400, detail="Missing player data")
# Обрабатываем информацию о сессии
await self._process_player_session(server_ip, player_id, player_name, duration)
return {"status": "success"}
# Если тип события не распознан
print(f"[{datetime.now()}] Неизвестное событие: {event_data}")
raise HTTPException(status_code=400, detail="Invalid event type")
except HTTPException as e:
print(f"[{datetime.now()}] Ошибка обработки события: {e.status_code}: {e.detail}")
print(f"Полученные данные: {event_data}")
raise
except Exception as e:
print(f"[{datetime.now()}] Необработанная ошибка: {str(e)}")
print(f"Полученные данные: {event_data}")
raise HTTPException(status_code=500, detail=f"Server error: {str(e)}")
async def _register_server(self, server_ip, event_data):
"""Регистрирует сервер, если его нет в базе"""
from app.db.database import db
import uuid
game_servers_collection = db.game_servers
# Проверяем, есть ли уже такой сервер
existing_server = await game_servers_collection.find_one({"ip": server_ip})
if not existing_server:
# Создаем новую запись сервера
server_data = {
"id": str(uuid.uuid4()),
"name": f"Server {server_ip}", # Можно улучшить название
"ip": server_ip,
"port": 25565, # Стандартный порт Minecraft
"description": f"Minecraft server {server_ip}",
"max_players": 100,
"registered_at": datetime.utcnow(),
"last_activity": datetime.utcnow() # Добавляем поле last_activity
}
await game_servers_collection.insert_one(server_data)
print(f"[{datetime.utcnow()}] Зарегистрирован новый сервер: {server_ip}")
else:
# Обновляем активность существующего сервера
await game_servers_collection.update_one(
{"ip": server_ip},
{"$set": {"last_activity": datetime.utcnow()}}
)
return existing_server or await game_servers_collection.find_one({"ip": server_ip})
async def _update_online_players(self, server_ip, players_data):
"""Обновляет информацию об онлайн игроках"""
from app.db.database import db
online_players_collection = db.online_players
game_servers_collection = db.game_servers
# Получаем ID сервера
server = await self._register_server(server_ip, {})
server_id = server["id"]
# Обновляем время активности сервера
await game_servers_collection.update_one(
{"id": server_id},
{"$set": {"last_activity": datetime.utcnow()}}
)
# Помечаем всех игроков как оффлайн на этом сервере
await online_players_collection.update_many(
{"server_id": server_id},
{"$set": {"is_online": False}}
)
# Обновляем данные для каждого онлайн игрока
now = datetime.utcnow()
for player in players_data:
player_id = player.get("player_id")
player_name = player.get("player_name")
online_time = player.get("online_time", 0)
if not player_id or not player_name:
continue
# Проверяем, существует ли уже запись
existing_player = await online_players_collection.find_one({
"uuid": player_id,
"server_id": server_id
})
if existing_player:
# Обновляем существующую запись
await online_players_collection.update_one(
{"_id": existing_player["_id"]},
{"$set": {
"username": player_name,
"is_online": True,
"last_seen": now,
"online_duration": online_time
}}
)
else:
# Создаем новую запись
await online_players_collection.insert_one({
"uuid": player_id,
"username": player_name,
"server_id": server_id,
"is_online": True,
"login_time": now,
"last_seen": now,
"online_duration": online_time
})
online_count = len(players_data)
print(f"[{now}] Обновлена информация о {online_count} игроках на сервере {server_ip}")
# Также обновляем информацию о коинах для каждого игрока
if players_data:
from app.services.coins import CoinsService
coins_service = CoinsService()
for player in players_data:
player_id = player.get("player_id")
player_name = player.get("player_name")
online_time = player.get("online_time", 0)
if player_id and player_name:
await coins_service.update_player_coins(player_id, player_name, online_time, server_ip)
async def _register_player_login(self, server_ip, player_id, player_name):
"""Регистрирует вход игрока на сервер"""
from app.db.database import db
online_players_collection = db.online_players
server = await self._register_server(server_ip, {})
server_id = server["id"]
now = datetime.utcnow()
# Проверяем, есть ли уже запись для этого игрока
existing_player = await online_players_collection.find_one({
"uuid": player_id,
"server_id": server_id
})
if existing_player:
# Обновляем запись
await online_players_collection.update_one(
{"_id": existing_player["_id"]},
{"$set": {
"username": player_name,
"is_online": True,
"login_time": now,
"last_seen": now
}}
)
else:
# Создаем новую запись
await online_players_collection.insert_one({
"uuid": player_id,
"username": player_name,
"server_id": server_id,
"is_online": True,
"login_time": now,
"last_seen": now,
"online_duration": 0
})
print(f"[{now}] Игрок {player_name} зашел на сервер {server_ip}")
async def _register_player_logout(self, server_ip, player_id, player_name):
"""Регистрирует выход игрока с сервера"""
from app.db.database import db
online_players_collection = db.online_players
server = await self._register_server(server_ip, {})
server_id = server["id"]
now = datetime.utcnow()
# Ищем запись игрока
player = await online_players_collection.find_one({
"uuid": player_id,
"server_id": server_id
})
if player:
# Обновляем запись
await online_players_collection.update_one(
{"_id": player["_id"]},
{"$set": {
"is_online": False,
"last_seen": now
}}
)
print(f"[{now}] Игрок {player_name} вышел с сервера {server_ip}")
async def _process_player_session(self, server_ip, player_id, player_name, duration):
"""Обрабатывает информацию о завершенной сессии игрока"""
from app.db.database import db
from app.services.coins import CoinsService
server = await self._register_server(server_ip, {})
server_id = server["id"]
# Обновляем статистику времени игры
await db.player_sessions.insert_one({
"uuid": player_id,
"username": player_name,
"server_id": server_id,
"server_ip": server_ip,
"duration": duration,
"session_end": datetime.utcnow()
})
# Начисляем коины за время игры
coins_service = CoinsService()
await coins_service.update_player_coins(player_id, player_name, duration, server_ip)
print(f"[{datetime.now()}] Сессия игрока {player_name} завершена, длительность: {duration} сек.")

View File

@ -0,0 +1,288 @@
from fastapi import HTTPException
from app.db.database import db, users_collection
from app.models.server.prank import PrankCommand, PrankCommandUpdate
from datetime import datetime, timedelta
import uuid
from app.services.server.command import CommandService
# Создаем коллекции для хранения пакостей и серверов
prank_commands_collection = db.prank_commands
game_servers_collection = db.game_servers
online_players_collection = db.online_players
class PrankService:
async def add_prank_command(self, command_data):
"""Добавление новой команды-пакости"""
# Проверяем корректность шаблона команды
if "{targetPlayer}" not in command_data.command_template:
raise HTTPException(status_code=400,
detail="Шаблон команды должен содержать {targetPlayer} для подстановки имени цели")
prank_id = str(uuid.uuid4())
# Создаем новую команду в БД
prank_command = {
"id": prank_id,
"name": command_data.name,
"description": command_data.description,
"price": command_data.price,
"command_template": command_data.command_template,
"server_ids": command_data.server_ids,
"targetDescription": command_data.targetDescription,
"globalDescription": command_data.globalDescription, # Добавить это поле
"created_at": datetime.utcnow()
}
await prank_commands_collection.insert_one(prank_command)
return {"status": "success", "id": prank_id}
async def get_all_prank_commands(self):
"""Получение списка всех команд-пакостей"""
commands = await prank_commands_collection.find().to_list(1000)
result = []
for cmd in commands:
result.append({
"id": cmd["id"],
"name": cmd["name"],
"description": cmd["description"],
"price": cmd["price"],
"command_template": cmd["command_template"],
"server_ids": cmd.get("server_ids", []),
"targetDescription": cmd.get("targetDescription"),
"globalDescription": cmd.get("globalDescription") # Добавить это поле
})
return result
async def get_prank_command(self, command_id: str):
"""Получение конкретной команды по ID"""
command = await prank_commands_collection.find_one({"id": command_id})
if not command:
raise HTTPException(status_code=404, detail="Команда не найдена")
return {
"id": command["id"],
"name": command["name"],
"description": command["description"],
"price": command["price"],
"command_template": command["command_template"],
"server_ids": command.get("server_ids", []),
"targetDescription": command.get("targetDescription"),
"globalDescription": command.get("globalDescription") # Добавить это поле
}
async def update_prank_command(self, command_id: str, update_data: PrankCommandUpdate):
"""Обновление команды-пакости"""
command = await prank_commands_collection.find_one({"id": command_id})
if not command:
raise HTTPException(status_code=404, detail="Команда не найдена")
# Готовим данные для обновления
update = {}
if update_data.name is not None:
update["name"] = update_data.name
if update_data.description is not None:
update["description"] = update_data.description
if update_data.price is not None:
update["price"] = update_data.price
if update_data.command_template is not None:
if "{targetPlayer}" not in update_data.command_template:
raise HTTPException(status_code=400,
detail="Шаблон команды должен содержать {targetPlayer} для подстановки имени цели")
update["command_template"] = update_data.command_template
if update_data.server_ids is not None:
update["server_ids"] = update_data.server_ids
if update_data.targetDescription is not None:
update["targetDescription"] = update_data.targetDescription
if update_data.globalDescription is not None: # Добавить эту проверку
update["globalDescription"] = update_data.globalDescription
if update:
result = await prank_commands_collection.update_one(
{"id": command_id},
{"$set": update}
)
if result.modified_count == 0:
raise HTTPException(status_code=500, detail="Ошибка при обновлении команды")
return {"status": "success"}
async def delete_prank_command(self, command_id: str):
"""Удаление команды-пакости"""
command = await prank_commands_collection.find_one({"id": command_id})
if not command:
raise HTTPException(status_code=404, detail="Команда не найдена")
result = await prank_commands_collection.delete_one({"id": command_id})
if result.deleted_count == 0:
raise HTTPException(status_code=500, detail="Ошибка при удалении команды")
return {"status": "success"}
async def get_all_servers(self):
"""Получение списка всех доступных серверов"""
# Проверяем и удаляем неактивные серверы (более 5 минут без данных)
current_time = datetime.utcnow()
inactive_threshold = 5 * 60 # 5 минут в секундах
# Находим серверы, которые не отправляли данные больше 5 минут
# Учитываем, что у некоторых серверов может не быть поля last_activity
inactive_servers = await game_servers_collection.find({
"last_activity": {
"$exists": True,
"$lt": current_time - timedelta(seconds=inactive_threshold)
}
}).to_list(100)
# Удаляем неактивные серверы
if inactive_servers:
server_ids = [server["id"] for server in inactive_servers]
await game_servers_collection.delete_many({"id": {"$in": server_ids}})
# Опционально: логирование удаленных серверов
for server in inactive_servers:
print(f"Удален неактивный сервер: {server['name']} (ID: {server['id']})")
# Получаем актуальный список серверов
servers = await game_servers_collection.find().to_list(100)
# Если нет зарегистрированных серверов, вернем пустой список
if not servers:
return []
result = []
for server in servers:
# Получаем количество онлайн игроков
online_count = await online_players_collection.count_documents(
{"server_id": server["id"], "is_online": True}
)
result.append({
"id": server["id"],
"name": server["name"],
"ip": server.get("ip"),
"port": server.get("port"),
"description": server.get("description", ""),
"online_players": online_count,
"max_players": server.get("max_players", 0),
"last_activity": server.get("last_activity")
})
return result
async def get_server_online_players(self, server_id: str):
"""Получение списка онлайн игроков на конкретном сервере"""
server = await game_servers_collection.find_one({"id": server_id})
if not server:
raise HTTPException(status_code=404, detail="Сервер не найден")
players = await online_players_collection.find(
{"server_id": server_id, "is_online": True}
).to_list(1000)
result = []
for player in players:
result.append({
"username": player["username"],
"uuid": player.get("uuid", ""),
"online_since": player.get("login_time")
})
return {
"server": {
"id": server["id"],
"name": server["name"]
},
"online_players": result,
"count": len(result)
}
async def execute_prank(self, username: str, command_id: str, target_player: str, server_id: str):
"""Выполнение пакости (покупка и выполнение команды)"""
# Проверяем пользователя
user = await users_collection.find_one({"username": username})
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Проверяем команду
command = await prank_commands_collection.find_one({"id": command_id})
if not command:
raise HTTPException(status_code=404, detail="Команда не найдена")
# Проверяем сервер
server = await game_servers_collection.find_one({"id": server_id})
if not server:
raise HTTPException(status_code=404, detail="Сервер не найден")
# Проверяем, доступна ли команда на данном сервере
if (command.get("server_ids") and
"*" not in command.get("server_ids", []) and
server_id not in command.get("server_ids", [])):
raise HTTPException(status_code=400, detail="Команда недоступна на выбранном сервере")
# Проверяем, онлайн ли целевой игрок
target_online = await online_players_collection.find_one({
"username": target_player,
"server_id": server_id,
"is_online": True
})
if not target_online:
raise HTTPException(status_code=400, detail=f"Игрок {target_player} не в сети на этом сервере")
# Проверяем достаточно ли монет
user_coins = user.get("coins", 0)
if user_coins < command["price"]:
raise HTTPException(status_code=400,
detail=f"Недостаточно монет. Требуется: {command['price']}, имеется: {user_coins}")
# Формируем команду для выполнения
actual_command = command["command_template"].replace("{targetPlayer}", target_player)
# Обрабатываем оба типа сообщений
target_desc = None
global_desc = None
if command.get("targetDescription"):
target_desc = command.get("targetDescription").replace("{username}", username).replace("{targetPlayer}", target_player)
if command.get("globalDescription"):
global_desc = command.get("globalDescription").replace("{username}", username).replace("{targetPlayer}", target_player)
# Отправляем команду с обоими сообщениями
command_service = CommandService()
from app.models.server.command import ServerCommand
server_command = ServerCommand(
command=actual_command,
server_ip=server.get("ip", ""),
require_online_player=True,
target_message=target_desc, # Сообщение для цели
global_message=global_desc # Сообщение для всех остальных
)
command_result = await command_service.add_command(server_command)
# Логируем выполнение пакости
log_entry = {
"user_id": user["_id"],
"username": username,
"target_player": target_player,
"command_id": command_id,
"command_name": command["name"],
"server_id": server_id,
"price": command["price"],
"executed_command": actual_command,
"executed_at": datetime.utcnow()
}
await db.prank_executions.insert_one(log_entry)
return {
"status": "success",
"message": f"Команда '{command['name']}' успешно выполнена на игроке {target_player}",
"remaining_coins": user_coins - command["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)
# Генерируем имя файла

314
app/services/store_cape.py Normal file
View File

@ -0,0 +1,314 @@
from fastapi import HTTPException, UploadFile
from app.db.database import users_collection
from app.core.config import FILES_URL
from datetime import datetime
import uuid
from pathlib import Path
import os
import shutil
from app.models.cape import CapeStore, CapeStoreUpdate
# Создаем коллекцию для плащей в БД
from app.db.database import db
store_capes_collection = db.store_capes
class StoreCapeService:
async def add_cape(self, name: str, description: str, price: int, cape_file: UploadFile):
"""Добавление нового плаща в магазин"""
# Проверка типа файла
if not cape_file.content_type.startswith('image/'):
raise HTTPException(status_code=400, detail="Файл должен быть изображением")
# Определяем расширение
ext = None
if cape_file.content_type == "image/png":
ext = "png"
elif cape_file.content_type == "image/gif":
ext = "gif"
else:
raise HTTPException(status_code=400, detail="Поддерживаются только PNG и GIF плащи")
# Проверка размера файла (максимум 2MB)
max_size = 2 * 1024 * 1024 # 2MB
contents = await cape_file.read()
if len(contents) > max_size:
raise HTTPException(status_code=400, detail="Файл слишком большой (максимум 2MB)")
# Создаем папку для плащей магазина, если ее нет
cape_dir = Path("/app/static/capes_store")
cape_dir.mkdir(parents=True, exist_ok=True)
# Генерируем ID и имя файла
cape_id = str(uuid.uuid4())
cape_filename = f"store_cape_{cape_id}.{ext}"
cape_path = cape_dir / cape_filename
# Сохраняем файл
with open(cape_path, "wb") as f:
f.write(contents)
# Создаем запись в БД
cape_data = {
"id": cape_id,
"name": name,
"description": description,
"price": price,
"file_name": cape_filename,
"created_at": datetime.utcnow()
}
await store_capes_collection.insert_one(cape_data)
return {"id": cape_id, "status": "success"}
async def get_all_capes(self):
"""Получение всех плащей из магазина"""
capes = await store_capes_collection.find().to_list(1000)
result = []
for cape in capes:
result.append({
"id": cape["id"],
"name": cape["name"],
"description": cape["description"],
"price": cape["price"],
"image_url": f"{FILES_URL}/capes_store/{cape['file_name']}"
})
return result
async def get_cape_by_id(self, cape_id: str):
"""Получение плаща по ID"""
cape = await store_capes_collection.find_one({"id": cape_id})
if not cape:
raise HTTPException(status_code=404, detail="Плащ не найден")
return {
"id": cape["id"],
"name": cape["name"],
"description": cape["description"],
"price": cape["price"],
"image_url": f"{FILES_URL}/capes_store/{cape['file_name']}"
}
async def update_cape(self, cape_id: str, update_data: CapeStoreUpdate):
"""Обновление информации о плаще"""
cape = await store_capes_collection.find_one({"id": cape_id})
if not cape:
raise HTTPException(status_code=404, detail="Плащ не найден")
# Готовим данные для обновления
update = {}
if update_data.name:
update["name"] = update_data.name
if update_data.description:
update["description"] = update_data.description
if update_data.price is not None:
update["price"] = update_data.price
if update:
result = await store_capes_collection.update_one(
{"id": cape_id},
{"$set": update}
)
if result.modified_count == 0:
raise HTTPException(status_code=500, detail="Ошибка при обновлении")
return {"status": "success"}
async def delete_cape(self, cape_id: str):
"""Удаление плаща из магазина"""
cape = await store_capes_collection.find_one({"id": cape_id})
if not cape:
raise HTTPException(status_code=404, detail="Плащ не найден")
# Удаляем файл
cape_path = Path(f"/app/static/capes_store/{cape['file_name']}")
if cape_path.exists():
try:
cape_path.unlink()
except Exception as e:
print(f"Ошибка при удалении файла: {e}")
# Удаляем из БД плащей магазина
result = await store_capes_collection.delete_one({"id": cape_id})
if result.deleted_count == 0:
raise HTTPException(status_code=500, detail="Ошибка при удалении из БД")
# Удаляем из БД купленных плащей
purchases_collection = db.purchases
purchases = await purchases_collection.find_one({"cape_id": cape_id})
if purchases:
await purchases_collection.delete_one({"cape_id": cape_id})
# Удаляем плащ из массива purchased_capes всех пользователей
users_collection = db.users
await users_collection.update_many(
{"purchased_capes.cape_id": cape_id},
{"$pull": {"purchased_capes": {"cape_id": cape_id}}}
)
return {"status": "success"}
async def purchase_cape(self, username: str, cape_id: str):
"""Покупка плаща пользователем"""
# Находим пользователя
user = await users_collection.find_one({"username": username})
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Находим плащ
cape = await store_capes_collection.find_one({"id": cape_id})
if not cape:
raise HTTPException(status_code=404, detail="Плащ не найден")
# Проверяем достаточно ли монет
user_coins = user.get("coins", 0)
if user_coins < cape["price"]:
raise HTTPException(status_code=400,
detail=f"Недостаточно монет. Требуется: {cape['price']}, имеется: {user_coins}")
# Копируем плащ из хранилища магазина в персональную папку пользователя
cape_store_path = Path(f"/app/static/capes_store/{cape['file_name']}")
# Создаем папку для плащей пользователя
cape_dir = Path("/app/static/capes")
cape_dir.mkdir(parents=True, exist_ok=True)
# Генерируем имя файла для персонального плаща
filename_parts = cape['file_name'].split('.')
ext = filename_parts[-1]
cape_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}"
cape_path = cape_dir / cape_filename
# Копируем файл
try:
shutil.copy(cape_store_path, cape_path)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при копировании файла: {e}")
# Обновляем данные пользователя
# 1. Списываем монеты
# 2. Устанавливаем новый плащ
# 3. Добавляем плащ в список приобретенных
result = await users_collection.update_one(
{"username": username},
{"$set": {
"coins": user_coins - cape["price"],
"cloak_url": f"{FILES_URL}/capes/{cape_filename}"
},
"$push": {
"purchased_capes": {
"cape_id": cape_id,
"cape_name": cape["name"],
"cape_description": cape["description"],
"file_name": cape_filename,
"purchased_at": datetime.utcnow()
}
}}
)
if result.modified_count == 0:
# Если обновление не удалось, удаляем файл плаща
if os.path.exists(cape_path):
os.remove(cape_path)
raise HTTPException(status_code=500, detail="Ошибка при обновлении данных пользователя")
# Логируем покупку в БД
purchase_data = {
"username": username,
"user_id": user["_id"],
"cape_id": cape_id,
"cape_name": cape["name"],
"price": cape["price"],
"purchase_date": datetime.utcnow()
}
from app.db.database import db
await db.purchases.insert_one(purchase_data)
return {
"status": "success",
"message": f"Плащ '{cape['name']}' успешно приобретен",
"remaining_coins": user_coins - cape["price"]
}
async def get_user_purchased_capes(self, username: str):
"""Получение всех плащей, приобретенных пользователем"""
user = await users_collection.find_one({"username": username})
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
purchased_capes = user.get("purchased_capes", [])
result = []
for cape in purchased_capes:
result.append({
"cape_id": cape.get("cape_id"),
"cape_name": cape.get("cape_name"),
"cape_description": cape.get("cape_description"),
"image_url": f"{FILES_URL}/capes/{cape.get('file_name')}",
"purchased_at": cape.get("purchased_at"),
"is_active": user.get("cloak_url") == f"{FILES_URL}/capes/{cape.get('file_name')}"
})
return result
async def activate_purchased_cape(self, username: str, cape_id: str):
"""Активация приобретенного плаща"""
user = await users_collection.find_one({"username": username})
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Проверяем, что плащ был приобретен
purchased_capes = user.get("purchased_capes", [])
selected_cape = None
for cape in purchased_capes:
if cape.get("cape_id") == cape_id:
selected_cape = cape
break
if not selected_cape:
raise HTTPException(status_code=404, detail="Плащ не найден среди приобретенных")
# Устанавливаем выбранный плащ
await users_collection.update_one(
{"username": username},
{"$set": {"cloak_url": f"{FILES_URL}/capes/{selected_cape.get('file_name')}"}}
)
return {
"status": "success",
"message": f"Плащ '{selected_cape.get('cape_name')}' активирован"
}
async def deactivate_purchased_cape(self, username: str, cape_id: str):
"""Деактивация приобретенного плаща"""
user = await users_collection.find_one({"username": username})
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Проверяем, что плащ был приобретен
purchased_capes = user.get("purchased_capes", [])
selected_cape = None
for cape in purchased_capes:
if cape.get("cape_id") == cape_id:
selected_cape = cape
break
if not selected_cape:
raise HTTPException(status_code=404, detail="Плащ не найден среди приобретенных")
# Устанавливаем выбранный плащ
await users_collection.update_one(
{"username": username},
{"$set": {"cloak_url": None}}
)
return {
"status": "success",
"message": f"Плащ '{selected_cape.get('cape_name')}' деактивирован"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

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
from app.api import users, skins, capes, meta, server, store, pranks, marketplace, bonuses
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
@ -9,13 +9,18 @@ app.include_router(meta.router)
app.include_router(users.router)
app.include_router(skins.router)
app.include_router(capes.router)
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")
app.mount("/capes", StaticFiles(directory="app/static/capes"), name="capes")
app.mount("/capes_store", StaticFiles(directory="app/static/capes_store"), name="capes_store")
# CORS, middleware и т.д.
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],

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)