26 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
16 changed files with 263 additions and 59 deletions

View File

@ -5,37 +5,27 @@ on:
branches: [main] branches: [main]
jobs: jobs:
build: deploy:
runs-on: ubuntu-latest runs-on: docker
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Set up Docker Buildx - name: Create .env file
uses: docker/setup-buildx-action@v2 run: |
echo "MONGO_URI=${{ secrets.MONGO_URI }}" > /home/server/popa_minecraft_launcher_api/.env
echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> /home/server/popa_minecraft_launcher_api/.env
echo "FILES_URL=${{ secrets.FILES_URL }}" >> /home/server/popa_minecraft_launcher_api/.env
echo "TELEGRAM_BOT_TOKEN=${{ secrets.TELEGRAM_BOT_TOKEN }}" >> /home/server/popa_minecraft_launcher_api/.env
echo "API_URL=${{ secrets.API_URL }}" >> /home/server/popa_minecraft_launcher_api/.env
- name: Login to Gitea Container Registry - name: Build and deploy
uses: docker/login-action@v2 run: |
with: cd /home/server/popa_minecraft_launcher_api
registry: git.popa-popa.ru git reset --hard HEAD
username: ${{ secrets.USERNAME }} git checkout main
password: ${{ secrets.PASSWORD }} git pull
docker-compose down -v
- name: Build and push Docker image docker-compose build
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: git.popa-popa.ru/DIKER/minecraft-api:latest # Замените username на ваше имя пользователя
- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /home/server/minecraft-api/
docker pull git.popa-popa.ru/DIKER/minecraft-api:latest
docker-compose up -d docker-compose up -d

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,21 +6,32 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
ports: ports:
- "3001:3000" - "3001:3000"
user: "${UID:-1000}:${GID:-1000}"
volumes: volumes:
- ./app/static:/app/static - ./app/static:/app/static:rw
environment: env_file:
- MONGO_URI=mongodb://mongodb:27017/minecraft-api - .env
- SECRET_KEY=your-secret-key
- ALGORITHM=HS256
- ACCESS_TOKEN_EXPIRE_MINUTES=30
depends_on: depends_on:
- mongodb - mongodb
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"]
telegram_bot:
container_name: telegram_bot
build:
context: .
dockerfile: Dockerfile
user: "${UID:-1000}:${GID:-1000}"
volumes:
- ./telegram_bot.py:/app/telegram_bot.py
env_file:
- .env
command: ["python", "telegram_bot.py"]
mongodb: mongodb:
container_name: mongodb container_name: mongodb
image: mongo:latest image: mongo:latest
ports: ports:
- "27017:27017" - "32768:27017"
volumes: volumes:
- ./mongodb:/data/db - ./mongodb:/data/db
environment: environment:

View File

@ -8,9 +8,7 @@ RUN pip install -r requirements.txt
COPY . . COPY . .
VOLUME /app/static RUN mkdir -p /app/static/skins /app/static/capes /app/static/capes_store && \
chown -R 1000:1000 /app/static
EXPOSE 3000 EXPOSE 3000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"]

View File

@ -15,9 +15,9 @@ app.include_router(pranks.router)
app.include_router(marketplace.router) app.include_router(marketplace.router)
# Монтируем статику # Монтируем статику
app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins") app.mount("/skins", StaticFiles(directory="/app/static/skins"), name="skins")
app.mount("/capes", StaticFiles(directory="app/static/capes"), name="capes") 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("/capes_store", StaticFiles(directory="/app/static/capes_store"), name="capes_store")
# CORS, middleware и т.д. # CORS, middleware и т.д.
app.add_middleware( app.add_middleware(

View File

@ -8,6 +8,7 @@ python-multipart>=0.0.9
mongoengine>=0.24.2 mongoengine>=0.24.2
python-dotenv>=1.0.0 python-dotenv>=1.0.0
pydantic>=2.0.0 pydantic>=2.0.0
pydantic[email]>=2.0.0
cryptography>=43.0.0 cryptography>=43.0.0
pytelegrambotapi>=2.0.0
httpx>=0.27.2

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)