This commit is contained in:
commit
279b2a7e90
49 files changed
+2664
No files matched your search
@@ -0,0 +1,3 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("loon")
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,35 @@
|
||||
import json
|
||||
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from loon.web.db import engine
|
||||
from loon.web.users.models import User
|
||||
from loon.web.utils.password import hash_password
|
||||
|
||||
|
||||
async def change_password_handler(client, userdata, msg):
|
||||
data = json.loads(msg.payload.decode())
|
||||
|
||||
if msg.topic != "loon/register/change_password":
|
||||
return
|
||||
|
||||
uuid = data["uuid"]
|
||||
password = data["password"]
|
||||
|
||||
with Session(engine) as session:
|
||||
statement = select(User).where(User.uuid == uuid)
|
||||
user = session.exec(statement).first()
|
||||
|
||||
if user is None:
|
||||
client.publish(f"loon/register/{uuid}/response",
|
||||
json.dumps({"success": False, "error": "The user is not registered!"}))
|
||||
return
|
||||
|
||||
with Session(engine) as session:
|
||||
user = session.exec(
|
||||
select(User).where(User.uuid == uuid)
|
||||
).first()
|
||||
user.password = hash_password(password)
|
||||
session.commit()
|
||||
|
||||
client.publish(f"loon/register/{uuid}/response", json.dumps({"success": True, "error": None}))
|
||||
@@ -0,0 +1,105 @@
|
||||
import json
|
||||
import random
|
||||
|
||||
from loon.web.users.state import user_threads
|
||||
|
||||
PLAYER_EVENT_SUFFIXES = ("died", "joined", "left")
|
||||
|
||||
DEATH_BY_DAMAGE = {
|
||||
"fall": (
|
||||
"{username} fell from a high place",
|
||||
"{username} forgot to tie their shoes",
|
||||
),
|
||||
"fire": (
|
||||
"{username} forgot fire burns",
|
||||
"{username} was engulfed in flames",
|
||||
),
|
||||
"drown": (
|
||||
"{username} drowned",
|
||||
),
|
||||
"blast": (
|
||||
"{username} blew up",
|
||||
),
|
||||
"combat": (
|
||||
"{username} was slain",
|
||||
"{username} was pummeled to death",
|
||||
),
|
||||
"generic": (
|
||||
"{username} died",
|
||||
"{username} withered away",
|
||||
),
|
||||
}
|
||||
|
||||
DEATH_BY_KILLER = {
|
||||
"fall": (
|
||||
"{username} was doomed to fall by {killer}",
|
||||
),
|
||||
"fire": (
|
||||
"{username} walked into fire whilst fighting {killer}",
|
||||
),
|
||||
"drown": (
|
||||
"{username} drowned whilst trying to escape {killer}",
|
||||
),
|
||||
"blast": (
|
||||
"{username} was blown up by {killer}",
|
||||
),
|
||||
"combat": (
|
||||
"{username} was slain by {killer}",
|
||||
"{username} was killed by {killer}",
|
||||
"{username} was slain by {killer} using magic",
|
||||
"{username} got finished off by {killer}",
|
||||
),
|
||||
"generic": (
|
||||
"{username} was slain by {killer}",
|
||||
"{username} was killed by {killer}",
|
||||
),
|
||||
}
|
||||
|
||||
DEATH_BY_KILLER_DEFAULT = DEATH_BY_KILLER["combat"]
|
||||
|
||||
|
||||
def get_random(templates, **kwargs):
|
||||
return random.choice(templates).format(**kwargs)
|
||||
|
||||
|
||||
async def marquee_handler(client, userdata, msg):
|
||||
prefix = "loon/player/"
|
||||
|
||||
if not msg.topic.startswith(prefix):
|
||||
return
|
||||
|
||||
rest = msg.topic[len(prefix):]
|
||||
_, _, subtopic = rest.partition("/")
|
||||
|
||||
if subtopic not in PLAYER_EVENT_SUFFIXES:
|
||||
return
|
||||
|
||||
try:
|
||||
data = json.loads(msg.payload.decode())
|
||||
except json.JSONDecodeError:
|
||||
return
|
||||
|
||||
username = data["player"]["username"]
|
||||
|
||||
if subtopic == "died":
|
||||
killer = data.get("killer")
|
||||
damage_type = data.get("damageType")
|
||||
|
||||
if killer:
|
||||
templates = DEATH_BY_KILLER.get(damage_type, DEATH_BY_KILLER_DEFAULT)
|
||||
message = get_random(templates, username=username, killer=killer)
|
||||
elif damage_type in DEATH_BY_DAMAGE:
|
||||
message = get_random(DEATH_BY_DAMAGE[damage_type], username=username)
|
||||
elif damage_type:
|
||||
message = f"{username} died of {damage_type}"
|
||||
else:
|
||||
message = f"{username} died"
|
||||
elif subtopic == "joined":
|
||||
message = f"{username} joined the game"
|
||||
else:
|
||||
message = f"{username} left the game"
|
||||
|
||||
envelope = json.dumps({"topic": "server/marquee", "payload": {"message": message}})
|
||||
|
||||
for queue in list(user_threads.values()):
|
||||
await queue.put(envelope)
|
||||
@@ -0,0 +1,19 @@
|
||||
import json
|
||||
|
||||
from loon.web.users.state import user_threads
|
||||
|
||||
|
||||
async def player_handler(client, userdata, msg):
|
||||
data = json.loads(msg.payload.decode())
|
||||
|
||||
prefix = "loon/player/"
|
||||
|
||||
if not msg.topic.startswith(prefix):
|
||||
return
|
||||
|
||||
rest = msg.topic[len(prefix):]
|
||||
player_uuid, _, subtopic = rest.partition("/")
|
||||
|
||||
queue = user_threads.get(player_uuid)
|
||||
if queue is not None:
|
||||
await queue.put(json.dumps({"topic": subtopic, "payload": data}))
|
||||
@@ -0,0 +1,34 @@
|
||||
import json
|
||||
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from loon.web.db import engine
|
||||
from loon.web.users.models import User
|
||||
from loon.web.utils.password import hash_password
|
||||
|
||||
|
||||
async def register_request_handler(client, userdata, msg):
|
||||
data = json.loads(msg.payload.decode())
|
||||
|
||||
if msg.topic != "loon/register/request":
|
||||
return
|
||||
|
||||
uuid = data["uuid"]
|
||||
password = data["password"]
|
||||
internal_username = data["internalUsername"]
|
||||
|
||||
with Session(engine) as session:
|
||||
statement = select(User).where(User.uuid == uuid)
|
||||
user = session.exec(statement).first()
|
||||
|
||||
if user is not None:
|
||||
client.publish(f"loon/register/{uuid}/response",
|
||||
json.dumps({"success": False, "error": "The user is already registered!"}))
|
||||
return
|
||||
|
||||
with Session(engine) as session:
|
||||
user = User(uuid=uuid, password=hash_password(password), internal_username=internal_username, admin=False)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
client.publish(f"loon/register/{uuid}/response", json.dumps({"success": True, "error": None}))
|
||||
@@ -0,0 +1,18 @@
|
||||
import json
|
||||
|
||||
from loon.web.users.state import user_threads
|
||||
|
||||
|
||||
async def server_handler(client, userdata, msg):
|
||||
if msg.topic != "loon/server/info":
|
||||
return
|
||||
|
||||
try:
|
||||
data = json.loads(msg.payload.decode())
|
||||
except json.JSONDecodeError:
|
||||
return
|
||||
|
||||
envelope = json.dumps({"topic": "server/info", "payload": data})
|
||||
|
||||
for queue in list(user_threads.values()):
|
||||
await queue.put(envelope)
|
||||
@@ -0,0 +1,34 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
import redis
|
||||
|
||||
from config import settings
|
||||
from loon.redis.chunk import set_chunk_cache
|
||||
from loon.web.users.state import user_threads
|
||||
|
||||
|
||||
async def world_handler(client, userdata, msg):
|
||||
chunk_topic = re.compile(r"^loon/world/chunk/(-?\d+)/(-?\d+)$")
|
||||
|
||||
match = chunk_topic.match(msg.topic)
|
||||
|
||||
if not match:
|
||||
return
|
||||
|
||||
try:
|
||||
data = json.loads(msg.payload.decode())
|
||||
except json.JSONDecodeError:
|
||||
return
|
||||
|
||||
x, z = match.group(1), match.group(2)
|
||||
envelope = json.dumps({"topic": f"world/chunk/{x}/{z}", "payload": data})
|
||||
|
||||
if settings.redis.enabled:
|
||||
try:
|
||||
set_chunk_cache(int(x), int(z), data)
|
||||
except redis.RedisError:
|
||||
pass
|
||||
|
||||
for queue in list(user_threads.values()):
|
||||
await queue.put(envelope)
|
||||
@@ -0,0 +1,74 @@
|
||||
import asyncio
|
||||
import json
|
||||
import threading
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from config import settings
|
||||
from loon import logger
|
||||
from loon.mqtt.handlers.change_password_request import change_password_handler
|
||||
from loon.mqtt.handlers.marquee import marquee_handler
|
||||
from loon.mqtt.handlers.player import player_handler
|
||||
from loon.mqtt.handlers.register_request import register_request_handler
|
||||
from loon.mqtt.handlers.server import server_handler
|
||||
from loon.mqtt.handlers.world import world_handler
|
||||
|
||||
|
||||
class MQTTManager:
|
||||
BASE_HANDLERS = [player_handler, register_request_handler, world_handler, server_handler, marquee_handler, change_password_handler]
|
||||
|
||||
def __init__(self, inject_handlers = None):
|
||||
if inject_handlers is None:
|
||||
inject_handlers = []
|
||||
|
||||
self.client = mqtt.Client()
|
||||
self.thread = None
|
||||
self.loop = asyncio.get_running_loop()
|
||||
|
||||
self.handlers = self.BASE_HANDLERS + inject_handlers
|
||||
|
||||
@staticmethod
|
||||
def setup_and_start():
|
||||
mqtt_client = MQTTManager()
|
||||
mqtt_client.connect()
|
||||
mqtt_client.start()
|
||||
return mqtt_client
|
||||
|
||||
def connect(self):
|
||||
self.client.on_connect = self.on_connect
|
||||
self.client.on_disconnect = self.on_disconnect
|
||||
self.client.on_message = self.on_topic
|
||||
self.client.username_pw_set(username=settings.mqtt.username, password=settings.mqtt.password)
|
||||
self.client.reconnect_delay_set(min_delay=1, max_delay=60)
|
||||
self.client.connect_async(settings.mqtt.host, settings.mqtt.port)
|
||||
|
||||
def start(self):
|
||||
self.thread = threading.Thread(target=self.client.loop_forever, daemon=True)
|
||||
self.thread.start()
|
||||
return self.thread
|
||||
|
||||
@staticmethod
|
||||
def on_connect(client, userdata, flags, reason_code):
|
||||
logger.info(f"MQTT connected (rc={reason_code})")
|
||||
client.subscribe("loon/#")
|
||||
|
||||
@staticmethod
|
||||
def on_disconnect(client, userdata, reason_code):
|
||||
logger.warning(f"MQTT disconnected (rc={reason_code}). Reconnecting...")
|
||||
|
||||
def on_topic(self, client, userdata, msg):
|
||||
async def handle_message():
|
||||
try:
|
||||
data = json.loads(msg.payload.decode())
|
||||
except json.decoder.JSONDecodeError:
|
||||
return
|
||||
|
||||
logger.info(f"[Received] {data}.")
|
||||
|
||||
for handler in self.handlers:
|
||||
await handler(client, userdata, msg)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(handle_message(), self.loop)
|
||||
|
||||
def publish(self, topic: str, payload: mqtt.PayloadType = None, qos: int = 0, retain: bool = False) -> mqtt.MQTTMessageInfo:
|
||||
return self.client.publish(topic, payload, qos, retain)
|
||||
@@ -0,0 +1,24 @@
|
||||
import redis
|
||||
from redis.backoff import ExponentialBackoff
|
||||
from redis.exceptions import ConnectionError, TimeoutError
|
||||
from redis.retry import Retry
|
||||
|
||||
from config import settings
|
||||
|
||||
r: redis.Redis | None = None
|
||||
if not settings.redis.enabled:
|
||||
r = None
|
||||
else:
|
||||
r = redis.Redis(
|
||||
host=settings.redis.host,
|
||||
port=settings.redis.port,
|
||||
password=settings.redis.password,
|
||||
decode_responses=True,
|
||||
socket_connect_timeout=2,
|
||||
socket_timeout=2,
|
||||
socket_keepalive=True,
|
||||
health_check_interval=30,
|
||||
retry_on_timeout=True,
|
||||
retry=Retry(ExponentialBackoff(cap=10, base=0.5), retries=0),
|
||||
retry_on_error=[ConnectionError, TimeoutError],
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
import json
|
||||
|
||||
from config import settings
|
||||
from loon.redis import r
|
||||
|
||||
|
||||
def set_chunk_cache(x: int, z: int, chunk_payload):
|
||||
r.setex(f'{x}:{z}', settings.redis.expire_time, str(json.dumps(chunk_payload)))
|
||||
|
||||
def get_chunk_cache(x: int, z: int):
|
||||
chunk = r.get(f'{x}:{z}')
|
||||
|
||||
if chunk:
|
||||
return json.loads(chunk)
|
||||
|
||||
return None
|
||||
|
||||
def reset_chunk_cache(x: int, z: int):
|
||||
return r.delete(f'{x}:{z}')
|
||||
@@ -0,0 +1,47 @@
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from starlette.middleware.authentication import AuthenticationMiddleware
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
from config import settings
|
||||
from loon.mqtt.manager import MQTTManager
|
||||
from loon.web.auth.middleware import JWTAuthBackend, on_auth_error
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
app.state.mqtt_manager = MQTTManager.setup_and_start()
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
app.add_middleware(
|
||||
AuthenticationMiddleware,
|
||||
backend=JWTAuthBackend(),
|
||||
on_error=on_auth_error,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.web.allowed_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
def get_mqtt_manager() -> MQTTManager:
|
||||
return app.state.mqtt_manager
|
||||
|
||||
from loon.web.auth.router import router as auth_router
|
||||
from loon.web.users.router import router as users_router
|
||||
from loon.web.world.router import router as world_router
|
||||
from loon.web.server.router import router as server_router
|
||||
from loon.web.admin.router import router as admin_router
|
||||
app.include_router(auth_router)
|
||||
app.include_router(users_router)
|
||||
app.include_router(world_router)
|
||||
app.include_router(server_router)
|
||||
app.include_router(admin_router)
|
||||
Whitespace-only changes.
@@ -0,0 +1,25 @@
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from loon.web.db import engine
|
||||
from loon.web.users.models import User
|
||||
|
||||
|
||||
def choose_admin(username: str) -> None:
|
||||
with Session(engine) as session:
|
||||
user = session.exec(
|
||||
select(User).where(User.internal_username == username)
|
||||
).first()
|
||||
|
||||
if user is None:
|
||||
print(f"No user named {username!r}.")
|
||||
return
|
||||
|
||||
if user.admin:
|
||||
print(f"{username} is already admin.")
|
||||
return
|
||||
|
||||
user.admin = True
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
print(f"Promoted {username} to admin.")
|
||||
@@ -0,0 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
from starlette.requests import Request
|
||||
|
||||
from loon.web.auth.middleware import admin
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,72 @@
|
||||
import jwt
|
||||
from starlette.authentication import (
|
||||
AuthCredentials,
|
||||
AuthenticationBackend,
|
||||
AuthenticationError,
|
||||
BaseUser,
|
||||
requires as _requires,
|
||||
)
|
||||
from starlette.requests import HTTPConnection
|
||||
from starlette.responses import JSONResponse, Response
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from loon.web.db import engine
|
||||
from loon.web.users.models import User
|
||||
from loon.web.utils.jwt import JWT_SECRET, JWT_ALGORITHM
|
||||
|
||||
authenticated = _requires("authenticated", status_code=401)
|
||||
admin = _requires(["authenticated", "admin"], status_code=403)
|
||||
|
||||
|
||||
class AuthenticatedUser(BaseUser):
|
||||
def __init__(self, user: User) -> None:
|
||||
self.user = user
|
||||
|
||||
@property
|
||||
def is_authenticated(self) -> bool:
|
||||
return True
|
||||
|
||||
class JWTAuthBackend(AuthenticationBackend):
|
||||
def _extract_token(self, conn: HTTPConnection) -> str | None:
|
||||
auth_header = conn.headers.get("Authorization")
|
||||
if auth_header is not None:
|
||||
scheme, _, credentials = auth_header.partition(" ")
|
||||
if scheme.lower() == "bearer" and credentials:
|
||||
return credentials
|
||||
|
||||
# Browser WebSocket APIs cannot set Authorization headers.
|
||||
token = conn.query_params.get("token")
|
||||
return token or None
|
||||
|
||||
async def authenticate(
|
||||
self, conn: HTTPConnection
|
||||
) -> tuple[AuthCredentials, AuthenticatedUser] | None:
|
||||
credentials = self._extract_token(conn)
|
||||
if credentials is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = jwt.decode(credentials, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
||||
except jwt.InvalidTokenError:
|
||||
raise AuthenticationError("Invalid or expired token")
|
||||
|
||||
user_id = payload.get("user_id")
|
||||
if not isinstance(user_id, int):
|
||||
raise AuthenticationError("Invalid or expired token")
|
||||
|
||||
with Session(engine) as session:
|
||||
user = session.exec(select(User).where(User.id == user_id)).first()
|
||||
|
||||
if user is None:
|
||||
raise AuthenticationError("User not found")
|
||||
|
||||
scopes = ["authenticated"]
|
||||
|
||||
if user.admin:
|
||||
scopes.append("admin")
|
||||
|
||||
return AuthCredentials(scopes), AuthenticatedUser(user)
|
||||
|
||||
|
||||
def on_auth_error(conn: HTTPConnection, exc: Exception) -> Response:
|
||||
return JSONResponse({"detail": str(exc)}, status_code=401)
|
||||
@@ -0,0 +1,26 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from loon.web.auth.schema import JWTResponse, JWTRequest
|
||||
from loon.web.db import engine
|
||||
from loon.web.users.models import User
|
||||
from loon.web.utils.jwt import generate_token
|
||||
from loon.web.utils.password import verify_password
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/jwt/", response_model=JWTResponse)
|
||||
async def get_jwt(body: JWTRequest) -> JWTResponse:
|
||||
with Session(engine) as session:
|
||||
statement = select(User).where(User.internal_username == body.username)
|
||||
user = session.exec(statement).first()
|
||||
|
||||
if user is None or not verify_password(body.password, user.password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid username or password",
|
||||
)
|
||||
|
||||
assert user.id is not None
|
||||
return JWTResponse(access_token=generate_token(user.id))
|
||||
@@ -0,0 +1,11 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class JWTRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class JWTResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
@@ -0,0 +1,24 @@
|
||||
from sqlmodel import SQLModel, create_engine
|
||||
|
||||
from config import settings
|
||||
|
||||
|
||||
def database_url(url: str | None = None) -> str:
|
||||
url = url or settings.database.url
|
||||
for src, dst in (
|
||||
("mysql://", "mysql+pymysql://"),
|
||||
("postgres://", "postgresql+psycopg://"),
|
||||
("postgresql://", "postgresql+psycopg://"),
|
||||
):
|
||||
if url.startswith(src):
|
||||
return dst + url.removeprefix(src)
|
||||
return url
|
||||
|
||||
|
||||
url = database_url()
|
||||
kwargs: dict = {}
|
||||
if url.startswith("sqlite"):
|
||||
kwargs["connect_args"] = {"check_same_thread": False}
|
||||
|
||||
engine = create_engine(url, echo=True, **kwargs)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
Whitespace-only changes.
@@ -0,0 +1,13 @@
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from loon.web import get_mqtt_manager
|
||||
from loon.web.auth.middleware import authenticated
|
||||
|
||||
router = APIRouter(prefix="/server", tags=["server"])
|
||||
|
||||
|
||||
@router.post("/info/request")
|
||||
@authenticated
|
||||
async def request_server_info(request: Request):
|
||||
get_mqtt_manager().publish("loon/server/info/request")
|
||||
return 200
|
||||
Whitespace-only changes.
@@ -0,0 +1,9 @@
|
||||
from sqlmodel import SQLModel, Field
|
||||
|
||||
|
||||
class User(SQLModel, table=True):
|
||||
id: int = Field(default=None, primary_key=True)
|
||||
uuid: str = Field(unique=True)
|
||||
internal_username: str
|
||||
password: str
|
||||
admin: bool = Field(default=False)
|
||||
@@ -0,0 +1,60 @@
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from starlette.websockets import WebSocket, WebSocketDisconnect
|
||||
|
||||
from loon.web import get_mqtt_manager
|
||||
from loon.web.auth.middleware import authenticated
|
||||
from loon.web.users.state import user_threads
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
@authenticated
|
||||
async def me(request: Request):
|
||||
return request.user.user.model_dump(exclude={"password", "id"})
|
||||
|
||||
@router.post("/inventory/request")
|
||||
@authenticated
|
||||
async def request_inventory(request: Request):
|
||||
user = request.user.user
|
||||
get_mqtt_manager().publish(f"loon/player/{user.uuid}/inventory/full/request")
|
||||
|
||||
return 200
|
||||
|
||||
@router.post("/online/request")
|
||||
@authenticated
|
||||
async def request_online(request: Request):
|
||||
user = request.user.user
|
||||
get_mqtt_manager().publish(f"loon/player/{user.uuid}/online/request")
|
||||
|
||||
return 200
|
||||
|
||||
@router.post("/position/request")
|
||||
@authenticated
|
||||
async def request_position(request: Request):
|
||||
user = request.user.user
|
||||
get_mqtt_manager().publish(f"loon/player/{user.uuid}/position/request")
|
||||
|
||||
return 200
|
||||
|
||||
|
||||
@router.websocket("/me")
|
||||
@authenticated
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
user = websocket.user.user
|
||||
|
||||
queue = asyncio.Queue()
|
||||
user_threads[user.uuid] = queue
|
||||
|
||||
try:
|
||||
while True:
|
||||
message = await queue.get()
|
||||
await websocket.send_text(message)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
user_threads.pop(user.uuid, None)
|
||||
@@ -0,0 +1 @@
|
||||
user_threads = {}
|
||||
Whitespace-only changes.
@@ -0,0 +1,20 @@
|
||||
import time
|
||||
|
||||
import jwt
|
||||
|
||||
from config import settings
|
||||
|
||||
JWT_SECRET = settings.jwt.secret
|
||||
JWT_ALGORITHM = "HS256"
|
||||
|
||||
|
||||
def generate_token(user_id: int) -> str:
|
||||
payload = {
|
||||
"user_id": user_id,
|
||||
"exp": int(time.time()) + 60 * 60 * 24,
|
||||
}
|
||||
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||
|
||||
def decode_token(token: str) -> dict:
|
||||
payload = jwt.decode(token, JWT_SECRET)
|
||||
return payload
|
||||
@@ -0,0 +1,19 @@
|
||||
from argon2 import PasswordHasher
|
||||
from argon2.exceptions import InvalidHashError, VerifyMismatchError
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
ph = PasswordHasher()
|
||||
|
||||
hashed = ph.hash(password)
|
||||
|
||||
return str(hashed)
|
||||
|
||||
|
||||
def verify_password(password: str, hashed_password: str) -> bool:
|
||||
ph = PasswordHasher()
|
||||
|
||||
try:
|
||||
return ph.verify(hashed_password, password)
|
||||
except (VerifyMismatchError, InvalidHashError):
|
||||
return False
|
||||
Whitespace-only changes.
@@ -0,0 +1,80 @@
|
||||
import json
|
||||
|
||||
import redis
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from config import settings
|
||||
from loon.redis.chunk import get_chunk_cache, reset_chunk_cache
|
||||
from loon.web import get_mqtt_manager
|
||||
from loon.web.auth.middleware import authenticated
|
||||
from loon.web.users.state import user_threads
|
||||
|
||||
router = APIRouter(prefix="/world", tags=["world"])
|
||||
|
||||
|
||||
@router.get("/request")
|
||||
@authenticated
|
||||
async def request_world(
|
||||
request: Request,
|
||||
x_start: int,
|
||||
x_end: int,
|
||||
z_start: int,
|
||||
z_end: int,
|
||||
):
|
||||
if not settings.redis.enabled:
|
||||
get_mqtt_manager().publish(f"loon/world/chunks/{x_start}:{x_end}/{z_start}:{z_end}/request")
|
||||
return 200
|
||||
|
||||
misses = []
|
||||
thread = user_threads.get(request.user.user.uuid)
|
||||
|
||||
try:
|
||||
for x in range(x_start, x_end + 1):
|
||||
for z in range(z_start, z_end + 1):
|
||||
cached_chunk = get_chunk_cache(x, z)
|
||||
|
||||
if cached_chunk is not None:
|
||||
if thread:
|
||||
await thread.put(
|
||||
json.dumps({"topic": f"world/chunk/{x}/{z}", "payload": cached_chunk})
|
||||
)
|
||||
else:
|
||||
misses.append((x, z))
|
||||
except redis.RedisError as e:
|
||||
get_mqtt_manager().publish(f"loon/world/chunks/{x_start}:{x_end}/{z_start}:{z_end}/request")
|
||||
return 200
|
||||
|
||||
for xa, xb, za, zb in miss_row_rects(misses):
|
||||
get_mqtt_manager().publish(f"loon/world/chunks/{xa}:{xb}/{za}:{zb}/request")
|
||||
|
||||
return 200
|
||||
|
||||
|
||||
def miss_row_rects(misses: list):
|
||||
rows = {}
|
||||
|
||||
for x, z in misses:
|
||||
if z not in rows:
|
||||
rows[z] = []
|
||||
rows[z].append(x)
|
||||
|
||||
rects = []
|
||||
|
||||
for z, xs in rows.items():
|
||||
xs.sort()
|
||||
|
||||
start = xs[0]
|
||||
end = xs[0]
|
||||
|
||||
for x in xs[1:]:
|
||||
if x == end + 1:
|
||||
end = x
|
||||
else:
|
||||
rects.append((start, end, z, z))
|
||||
start = x
|
||||
end = x
|
||||
|
||||
rects.append((start, end, z, z))
|
||||
|
||||
return rects
|
||||
|
||||
Reference in new issue
Block a user