Compare commits

..

28 Commits

Author SHA1 Message Date
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
17 changed files with 317 additions and 20 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

View File

@ -61,3 +61,22 @@ async def submit_item_details(data: dict):
"""Получить подробные данные о предмете"""
from app.services.marketplace import MarketplaceService
return await MarketplaceService().update_item_details(data["operation_id"], data["item_data"])
@router.delete("/items/{item_id}")
async def cancel_item_sale(
item_id: str,
username: str = Query(...)
):
"""Снять предмет с продажи"""
from app.services.marketplace import MarketplaceService
return await MarketplaceService().cancel_item_sale(username, item_id)
@router.put("/items/{item_id}/price")
async def update_item_price(
item_id: str,
new_price: int = Body(..., gt=0),
username: str = Body(...)
):
"""Обновить цену предмета на торговой площадке"""
from app.services.marketplace import MarketplaceService
return await MarketplaceService().update_item_price(username, item_id, new_price)

View File

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

View File

@ -1,5 +1,5 @@
from fastapi import APIRouter, HTTPException, Body, Response
from app.models.user import UserCreate, UserLogin
from app.models.user import UserCreate, UserLogin, VerifyCode
from app.models.request import ValidateRequest
from app.services.auth import AuthService
from app.db.database import users_collection, sessions_collection
@ -117,3 +117,15 @@ async def get_user_by_uuid(uuid: str):
safe_user["total_time_formatted"] = f"{hours}ч {minutes}м {seconds}с"
return safe_user
@router.post("/auth/verify_code")
async def verify_code(verify_code: VerifyCode):
return await AuthService().verify_code(verify_code.username, verify_code.code, verify_code.telegram_chat_id)
@router.post("/auth/generate_code")
async def generate_code(username: str):
return await AuthService().generate_code(username)
@router.get("/auth/verification_status/{username}")
async def get_verification_status(username: str):
return await AuthService().get_verification_status(username)

View File

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

View File

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

View File

@ -17,6 +17,7 @@ from cryptography.hazmat.primitives.asymmetric import padding
from dotenv import load_dotenv
import os
from pathlib import Path
import secrets
env_path = Path(__file__).parent.parent / ".env"
load_dotenv(dotenv_path=env_path)
@ -37,19 +38,66 @@ class AuthService:
# Сохраняем в MongoDB
new_user = UserInDB(
username=user.username,
email=user.email,
hashed_password=hashed_password,
uuid=user_uuid,
is_verified=False,
code=None,
code_expires_at=None
)
await users_collection.insert_one(new_user.dict())
return {"status": "success", "uuid": user_uuid}
async def generate_code(self, username: str):
if await users_collection.find_one({"username": username}):
if await users_collection.find_one({"username": username, "is_verified": True}):
raise HTTPException(400, "User already verified")
code = secrets.token_hex(3).upper()
await users_collection.update_one({"username": username}, {"$set": {"code": code, "code_expires_at": datetime.utcnow() + timedelta(minutes=10)}})
return {"status": "success", "code": code}
else:
raise HTTPException(404, "User not found")
async def verify_code(self, username: str, code: str, telegram_chat_id: int):
user = await users_collection.find_one({"username": username})
if not user:
raise HTTPException(404, "User not found")
if user["is_verified"]:
raise HTTPException(400, "User already verified")
# Проверяем код и привязку к Telegram
if user.get("telegram_chat_id") and user["telegram_chat_id"] != telegram_chat_id:
raise HTTPException(403, "This account is linked to another Telegram")
if user.get("code") != code:
raise HTTPException(400, "Invalid code")
# Обновляем chat_id при первом подтверждении
await users_collection.update_one(
{"username": username},
{"$set": {
"is_verified": True,
"telegram_chat_id": telegram_chat_id,
"code": None
}}
)
return {"status": "success"}
async def get_verification_status(self, username: str):
user = await users_collection.find_one({"username": username})
if not user:
raise HTTPException(404, "User not found")
return {"is_verified": user["is_verified"]}
async def login(self, credentials: UserLogin):
# Ищем пользователя
user = await users_collection.find_one({"username": credentials.username})
if not user or not verify_password(credentials.password, user["hashed_password"]):
raise HTTPException(status_code=401, detail="Invalid credentials")
if not user["is_verified"]:
raise HTTPException(status_code=401, detail="User not verified")
# Генерируем токены
access_token = create_access_token({"sub": user["username"], "uuid": user["uuid"]})
client_token = str(uuid.uuid4())

View File

@ -30,7 +30,7 @@ class CapeService:
import os
old_url = user["cloak_url"]
old_filename = os.path.basename(urlparse(old_url).path)
old_path = os.path.join("app/static/capes", old_filename)
old_path = os.path.join("/app/static/capes", old_filename)
if os.path.exists(old_path):
try:
os.remove(old_path)
@ -39,7 +39,7 @@ class CapeService:
# Создаем папку для плащей, если ее нет
from pathlib import Path
cape_dir = Path("app/static/capes")
cape_dir = Path("/app/static/capes")
cape_dir.mkdir(parents=True, exist_ok=True)
cape_filename = f"{username}_{int(datetime.now().timestamp())}.{ext}"

View File

@ -211,3 +211,68 @@ class MarketplaceService:
"operation_id": operation_id,
"message": "Покупка в обработке. Предмет будет добавлен в ваш инвентарь."
}
async def cancel_item_sale(self, username: str, item_id: str):
"""Снять предмет с продажи"""
# Находим предмет
item = await marketplace_collection.find_one({"id": item_id})
if not item:
raise HTTPException(status_code=404, detail="Предмет не найден")
# Проверяем, что пользователь является владельцем предмета
if item["seller_name"] != username:
raise HTTPException(status_code=403, detail="Вы не можете снять с продажи чужой предмет")
# Создаем операцию возврата предмета
operation_id = str(uuid.uuid4())
operation = {
"id": operation_id,
"type": "cancel_sale",
"player_name": username,
"item_id": item_id,
"item_data": item["item_data"],
"server_ip": item["server_ip"],
"status": "pending",
"created_at": datetime.utcnow()
}
await marketplace_operations.insert_one(operation)
# Удаляем предмет с торговой площадки
await marketplace_collection.delete_one({"id": item_id})
return {
"status": "pending",
"operation_id": operation_id,
"message": "Предмет снят с продажи и будет возвращен в ваш инвентарь"
}
async def update_item_price(self, username: str, item_id: str, new_price: int):
"""Обновить цену предмета на торговой площадке"""
# Находим предмет
item = await marketplace_collection.find_one({"id": item_id})
if not item:
raise HTTPException(status_code=404, detail="Предмет не найден")
# Проверяем, что пользователь является владельцем предмета
if item["seller_name"] != username:
raise HTTPException(status_code=403, detail="Вы не можете изменить цену чужого предмета")
# Валидация новой цены
if new_price <= 0:
raise HTTPException(status_code=400, detail="Цена должна быть положительным числом")
# Обновляем цену предмета
result = await marketplace_collection.update_one(
{"id": item_id},
{"$set": {"price": new_price}}
)
if result.modified_count == 0:
raise HTTPException(status_code=500, detail="Не удалось обновить цену предмета")
return {
"status": "success",
"message": f"Цена предмета обновлена на {new_price} монет"
}

View File

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

View File

@ -35,7 +35,7 @@ class StoreCapeService:
raise HTTPException(status_code=400, detail="Файл слишком большой (максимум 2MB)")
# Создаем папку для плащей магазина, если ее нет
cape_dir = Path("app/static/capes_store")
cape_dir = Path("/app/static/capes_store")
cape_dir.mkdir(parents=True, exist_ok=True)
# Генерируем ID и имя файла
@ -124,7 +124,7 @@ class StoreCapeService:
raise HTTPException(status_code=404, detail="Плащ не найден")
# Удаляем файл
cape_path = Path(f"app/static/capes_store/{cape['file_name']}")
cape_path = Path(f"/app/static/capes_store/{cape['file_name']}")
if cape_path.exists():
try:
cape_path.unlink()
@ -170,10 +170,10 @@ class StoreCapeService:
detail=f"Недостаточно монет. Требуется: {cape['price']}, имеется: {user_coins}")
# Копируем плащ из хранилища магазина в персональную папку пользователя
cape_store_path = Path(f"app/static/capes_store/{cape['file_name']}")
cape_store_path = Path(f"/app/static/capes_store/{cape['file_name']}")
# Создаем папку для плащей пользователя
cape_dir = Path("app/static/capes")
cape_dir = Path("/app/static/capes")
cape_dir.mkdir(parents=True, exist_ok=True)
# Генерируем имя файла для персонального плаща

40
docker-compose.yml Normal file
View File

@ -0,0 +1,40 @@
services:
app:
container_name: minecraft-api
build:
context: .
dockerfile: Dockerfile
ports:
- "3001:3000"
user: "${UID:-1000}:${GID:-1000}"
volumes:
- ./app/static:/app/static:rw
env_file:
- .env
depends_on:
- mongodb
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"]
telegram_bot:
container_name: telegram_bot
build:
context: .
dockerfile: Dockerfile
user: "${UID:-1000}:${GID:-1000}"
volumes:
- ./telegram_bot.py:/app/telegram_bot.py
env_file:
- .env
command: ["python", "telegram_bot.py"]
mongodb:
container_name: mongodb
image: mongo:latest
ports:
- "32768:27017"
volumes:
- ./mongodb:/data/db
environment:
- MONGO_INITDB_ROOT_USERNAME=popa
- MONGO_INITDB_ROOT_PASSWORD=2006sit_
restart: always

14
dockerfile Normal file
View File

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

View File

@ -15,9 +15,9 @@ app.include_router(pranks.router)
app.include_router(marketplace.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")
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(

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

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)