Compare commits

..
8 Commits
Author SHA1 Message Date
ciomek 2fee340259 Fixes
Build and Push Docker Image / build-and-push (push) Successful in 15s
2026-09-04 23:50:58 +02:00
ciomek 9964fbf044 No SQLModel 2026-09-04 23:41:35 +02:00
ciomek 5f0e8c11b4 User filter
Build and Push Docker Image / build-and-push (push) Successful in 19s
2026-09-04 18:03:21 +02:00
ciomek 655fb89a7f Users
Build and Push Docker Image / build-and-push (push) Successful in 27s
2026-09-04 17:52:08 +02:00
ciomek 632d990c85 Player format 2026-09-04 17:47:36 +02:00
ciomek 3dcd4173c2 Fixes 2026-09-04 17:41:24 +02:00
ciomek dbed39ee3a MC Users 2026-09-04 17:31:30 +02:00
ciomek 5732e26380 Logs 2026-09-04 17:02:25 +02:00
27 changed files with 426 additions and 102 deletions

No files matched your search

+2 -3
View File
@@ -4,9 +4,8 @@ from sqlalchemy import engine_from_config
from sqlalchemy import pool from sqlalchemy import pool
from alembic import context from alembic import context
from sqlmodel import SQLModel
from loon.web.db import database_url from loon.web.db import Base, database_url
import loon.web import loon.web
@@ -23,7 +22,7 @@ if config.config_file_name is not None:
# for 'autogenerate' support # for 'autogenerate' support
# from myapp import mymodel # from myapp import mymodel
# target_metadata = mymodel.Base.metadata # target_metadata = mymodel.Base.metadata
target_metadata = SQLModel.metadata target_metadata = Base.metadata
config.set_main_option("sqlalchemy.url", database_url()) config.set_main_option("sqlalchemy.url", database_url())
-2
View File
@@ -10,8 +10,6 @@ from typing import Sequence, Union
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
${imports if imports else ""} ${imports if imports else ""}
import sqlmodel
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)} revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
-42
View File
@@ -1,42 +0,0 @@
"""Users
Revision ID: 2df7e2c6af43
Revises:
Create Date: 2026-08-17 12:42:05.103510
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision: str = '2df7e2c6af43'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('internal_username', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('password', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('admin', sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('internal_username'),
sa.UniqueConstraint('uuid')
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
# ### end Alembic commands ###
+65
View File
@@ -0,0 +1,65 @@
"""init
Revision ID: 7efb7d0c39ae
Revises:
Create Date: 2026-09-04 23:50:22.513272
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '7efb7d0c39ae'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('logtype',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('code', sa.String(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('code')
)
op.create_table('minecraftuser',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(), nullable=False),
sa.Column('username', sa.String(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid')
)
op.create_table('logentry',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('log_type_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('message', sa.String(), nullable=False),
sa.ForeignKeyConstraint(['log_type_id'], ['logtype.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('minecraft_user_id', sa.Integer(), nullable=False),
sa.Column('internal_username', sa.String(), nullable=False),
sa.Column('password', sa.String(), nullable=False),
sa.Column('admin', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['minecraft_user_id'], ['minecraftuser.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('internal_username'),
sa.UniqueConstraint('minecraft_user_id')
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
op.drop_table('logentry')
op.drop_table('minecraftuser')
op.drop_table('logtype')
# ### end Alembic commands ###
+13 -7
View File
@@ -1,10 +1,13 @@
import json import json
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlmodel import Session, select from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web.db import engine from loon.web.db import engine
from loon.web.users.models import User from loon.web.logs.service import write_log, player_tag
from loon.web.logs.types import LogTypeEnum
from loon.web.users.models import MinecraftUser, User
async def change_password_handler(client, userdata, msg, data): async def change_password_handler(client, userdata, msg, data):
@@ -22,18 +25,19 @@ async def change_password_handler(client, userdata, msg, data):
password = data["password"] password = data["password"]
with Session(engine) as session: with Session(engine) as session:
statement = select(User).where(User.uuid == uuid) statement = select(User).join(MinecraftUser).where(MinecraftUser.uuid == uuid)
user = session.exec(statement).first() user = session.execute(statement).scalars().first()
if user is None: if user is None:
client.publish(f"loon/auth/change_password/{uuid}/response", client.publish(f"loon/auth/change_password/{uuid}/response",
json.dumps({"success": False, "error": "The user is not registered!"})) json.dumps({"success": False, "error": "The user is not registered!"}))
return return
user = session.exec( user = session.execute(
select(User).where(User.uuid == uuid) select(User).join(MinecraftUser).where(MinecraftUser.uuid == uuid)
).first() ).scalars().first()
user.password = password user.password = password
internal_username = user.internal_username
try: try:
session.commit() session.commit()
@@ -44,4 +48,6 @@ async def change_password_handler(client, userdata, msg, data):
return return
await write_log(LogTypeEnum.PLAYER_PASSWORD_CHANGED, f"Player {player_tag(uuid, internal_username)} changed their password.")
client.publish(f"loon/auth/change_password/{uuid}/response", json.dumps({"success": True, "error": None})) client.publish(f"loon/auth/change_password/{uuid}/response", json.dumps({"success": True, "error": None}))
+30 -2
View File
@@ -1,10 +1,14 @@
import json import json
import random import random
from loon.web.logs.service import write_log, player_tag
from loon.web.logs.types import LogTypeEnum
from loon.web.users.state import user_threads from loon.web.users.state import user_threads
PLAYER_EVENT_SUFFIXES = ("died", "joined", "left", "kicked") PLAYER_EVENT_SUFFIXES = ("died", "joined", "left", "kicked")
BAN_KICK_REASON = "Banned by admin"
DEATH_BY_DAMAGE = { DEATH_BY_DAMAGE = {
"fall": ( "fall": (
"{username} fell from a high place", "{username} fell from a high place",
@@ -78,7 +82,8 @@ async def marquee_handler(client, userdata, msg, data):
if not isinstance(player, dict): if not isinstance(player, dict):
return return
username = player.get("username") username = player.get("username")
if not username: uuid = player.get("uuid")
if not username or not uuid:
return return
if subtopic == "died": if subtopic == "died":
@@ -88,20 +93,43 @@ async def marquee_handler(client, userdata, msg, data):
if killer: if killer:
templates = DEATH_BY_KILLER.get(damage_type, DEATH_BY_KILLER_DEFAULT) templates = DEATH_BY_KILLER.get(damage_type, DEATH_BY_KILLER_DEFAULT)
message = get_random(templates, username=username, killer=killer) message = get_random(templates, username=username, killer=killer)
await write_log(LogTypeEnum.PLAYER_DEAD, f"Player {player_tag(uuid, username)} was killed by {killer}.")
elif damage_type in DEATH_BY_DAMAGE: elif damage_type in DEATH_BY_DAMAGE:
message = get_random(DEATH_BY_DAMAGE[damage_type], username=username) message = get_random(DEATH_BY_DAMAGE[damage_type], username=username)
await write_log(LogTypeEnum.PLAYER_DEAD, f"Player {player_tag(uuid, username)} died of {damage_type}.")
elif damage_type: elif damage_type:
message = f"{username} died of {damage_type}" message = f"{username} died of {damage_type}"
await write_log(LogTypeEnum.PLAYER_DEAD, f"Player {player_tag(uuid, username)} died of {damage_type}.")
else: else:
message = f"{username} died" message = f"{username} died"
await write_log(LogTypeEnum.PLAYER_DEAD, f"Player {player_tag(uuid, username)} died.")
elif subtopic == "joined": elif subtopic == "joined":
message = f"{username} joined the game" message = f"{username} joined the game"
await write_log(LogTypeEnum.PLAYER_JOINED, f"Player {player_tag(uuid, username)} joined the game.")
elif subtopic == "kicked": elif subtopic == "kicked":
reason = data.get("reason") reason = data.get("reason")
message = f"{username} was kicked: {reason}" if reason else f"{username} was kicked"
if reason == BAN_KICK_REASON:
message = f"{username} was banned"
await write_log(LogTypeEnum.PLAYER_BANNED, f"Player {player_tag(uuid, username)} was banned.")
else:
message = f"{username} was kicked: {reason}" if reason else f"{username} was kicked"
await write_log(
LogTypeEnum.PLAYER_KICKED,
f"Player {player_tag(uuid, username)} was kicked: {reason}." if reason else f"Player {player_tag(uuid, username)} was kicked.",
)
else: else:
message = f"{username} left the game" message = f"{username} left the game"
await write_log(LogTypeEnum.PLAYER_LEFT, f"Player {player_tag(uuid, username)} left the game.")
envelope = json.dumps({"topic": "server/marquee", "payload": {"message": message}}) envelope = json.dumps({"topic": "server/marquee", "payload": {"message": message}})
for queue in list(user_threads.values()): for queue in list(user_threads.values()):
+12
View File
@@ -0,0 +1,12 @@
from loon.web.users.service import sync_minecraft_users
async def player_joined_handler(client, userdata, msg, data):
if msg.topic != "loon/playerlist/joined":
return
player = data.get("player")
if not player:
return
await sync_minecraft_users([{"uuid": player["uuid"], "username": player["username"]}])
+8
View File
@@ -0,0 +1,8 @@
from loon.web.users.service import sync_minecraft_users
async def players_sync_handler(client, userdata, msg, data):
if msg.topic != "loon/playerlist/sync":
return
await sync_minecraft_users(data.get("players", []))
+18 -6
View File
@@ -1,10 +1,13 @@
import json import json
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlmodel import Session, select from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web.db import engine from loon.web.db import engine
from loon.web.users.models import User from loon.web.logs.service import write_log, player_tag
from loon.web.logs.types import LogTypeEnum
from loon.web.users.models import MinecraftUser, User
async def register_request_handler(client, userdata, msg, data): async def register_request_handler(client, userdata, msg, data):
@@ -23,8 +26,15 @@ async def register_request_handler(client, userdata, msg, data):
internal_username = data["internalUsername"] internal_username = data["internalUsername"]
with Session(engine) as session: with Session(engine) as session:
statement = select(User).where(User.uuid == uuid) minecraft_user = session.execute(select(MinecraftUser).where(MinecraftUser.uuid == uuid)).scalars().first()
user = session.exec(statement).first()
if minecraft_user is None:
client.publish(f"loon/auth/register/{uuid}/response",
json.dumps({"success": False, "error": "Player not recognized yet, please rejoin the server and try again."}))
return
statement = select(User).where(User.minecraft_user_id == minecraft_user.id)
user = session.execute(statement).scalars().first()
if user is not None: if user is not None:
client.publish(f"loon/auth/register/{uuid}/response", client.publish(f"loon/auth/register/{uuid}/response",
@@ -32,14 +42,14 @@ async def register_request_handler(client, userdata, msg, data):
return return
statement = select(User).where(User.internal_username == internal_username) statement = select(User).where(User.internal_username == internal_username)
user = session.exec(statement).first() user = session.execute(statement).scalars().first()
if user is not None: if user is not None:
client.publish(f"loon/auth/register/{uuid}/response", client.publish(f"loon/auth/register/{uuid}/response",
json.dumps({"success": False, "error": f"The {internal_username} is taken!"})) json.dumps({"success": False, "error": f"The {internal_username} is taken!"}))
return return
user = User(uuid=uuid, password=password, internal_username=internal_username, admin=False) user = User(minecraft_user_id=minecraft_user.id, password=password, internal_username=internal_username, admin=False)
session.add(user) session.add(user)
try: try:
session.commit() session.commit()
@@ -50,4 +60,6 @@ async def register_request_handler(client, userdata, msg, data):
return return
await write_log(LogTypeEnum.PLAYER_REGISTERED, f"Player {player_tag(uuid, internal_username)} registered.")
client.publish(f"loon/auth/register/{uuid}/response", json.dumps({"success": True, "error": None})) client.publish(f"loon/auth/register/{uuid}/response", json.dumps({"success": True, "error": None}))
+6 -3
View File
@@ -1,9 +1,10 @@
import json import json
from sqlmodel import Session, select from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web.db import engine from loon.web.db import engine
from loon.web.users.models import User from loon.web.users.models import MinecraftUser, User
async def show_username_handler(client, userdata, msg, data): async def show_username_handler(client, userdata, msg, data):
@@ -19,7 +20,9 @@ async def show_username_handler(client, userdata, msg, data):
return return
with Session(engine) as session: with Session(engine) as session:
user = session.exec(select(User).where(User.uuid == uuid)).first() user = session.execute(
select(User).join(MinecraftUser).where(MinecraftUser.uuid == uuid)
).scalars().first()
if user is None: if user is None:
client.publish(f"loon/auth/show_username/{uuid}/response", client.publish(f"loon/auth/show_username/{uuid}/response",
+5 -1
View File
@@ -9,6 +9,8 @@ from loon import logger
from loon.mqtt.handlers.change_password_request import change_password_handler from loon.mqtt.handlers.change_password_request import change_password_handler
from loon.mqtt.handlers.marquee import marquee_handler from loon.mqtt.handlers.marquee import marquee_handler
from loon.mqtt.handlers.player import player_handler from loon.mqtt.handlers.player import player_handler
from loon.mqtt.handlers.player_joined import player_joined_handler
from loon.mqtt.handlers.players_sync import players_sync_handler
from loon.mqtt.handlers.register_request import register_request_handler from loon.mqtt.handlers.register_request import register_request_handler
from loon.mqtt.handlers.server import server_handler from loon.mqtt.handlers.server import server_handler
from loon.mqtt.handlers.show_username_request import show_username_handler from loon.mqtt.handlers.show_username_request import show_username_handler
@@ -16,7 +18,7 @@ from loon.mqtt.handlers.world import world_handler
class MQTTManager: class MQTTManager:
BASE_HANDLERS = [player_handler, register_request_handler, world_handler, server_handler, marquee_handler, change_password_handler, show_username_handler] BASE_HANDLERS = [player_handler, register_request_handler, world_handler, server_handler, marquee_handler, change_password_handler, show_username_handler, players_sync_handler, player_joined_handler]
def __init__(self, inject_handlers = None): def __init__(self, inject_handlers = None):
if inject_handlers is None: if inject_handlers is None:
@@ -53,6 +55,8 @@ class MQTTManager:
logger.info(f"MQTT connected (rc={reason_code})") logger.info(f"MQTT connected (rc={reason_code})")
client.subscribe("loon/#") client.subscribe("loon/#")
client.publish("loon/playerlist/sync/request")
@staticmethod @staticmethod
def on_disconnect(client, userdata, reason_code): def on_disconnect(client, userdata, reason_code):
logger.warning(f"MQTT disconnected (rc={reason_code}). Reconnecting...") logger.warning(f"MQTT disconnected (rc={reason_code}). Reconnecting...")
+4 -3
View File
@@ -1,4 +1,5 @@
from sqlmodel import Session, select from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web.db import engine from loon.web.db import engine
from loon.web.users.models import User from loon.web.users.models import User
@@ -6,9 +7,9 @@ from loon.web.users.models import User
def choose_admin(username: str) -> None: def choose_admin(username: str) -> None:
with Session(engine) as session: with Session(engine) as session:
user = session.exec( user = session.execute(
select(User).where(User.internal_username == username) select(User).where(User.internal_username == username)
).first() ).scalars().first()
if user is None: if user is None:
print(f"No user named {username!r}.") print(f"No user named {username!r}.")
+79 -1
View File
@@ -1,7 +1,85 @@
from fastapi import APIRouter from fastapi import APIRouter, HTTPException
from sqlalchemy.orm import selectinload, Session
from sqlalchemy import select
from starlette.requests import Request from starlette.requests import Request
from loon.web.auth.middleware import admin from loon.web.auth.middleware import admin
from loon.web.db import engine
from loon.web.logs.models import LogEntry
from loon.web.logs.schema import LogEntryResponse
from loon.web.users.models import MinecraftUser, User
from loon.web.users.schema import MinecraftUserPublic, UserPublic
router = APIRouter(prefix="/admin", tags=["admin"]) router = APIRouter(prefix="/admin", tags=["admin"])
@router.get("/logs", response_model=list[LogEntryResponse])
@admin
async def list_logs(request: Request):
with Session(engine) as session:
statement = (
select(LogEntry)
.options(selectinload(LogEntry.log_type))
.order_by(LogEntry.created_at)
)
entries = session.execute(statement).scalars().all()
return [
LogEntryResponse(
id=entry.id,
log_type=entry.log_type.code,
created_at=entry.created_at,
message=entry.message,
)
for entry in entries
]
@router.get("/mc-users", response_model=list[MinecraftUserPublic])
@admin
async def list_mc_users(request: Request):
with Session(engine) as session:
statement = select(MinecraftUser).order_by(MinecraftUser.username)
return session.execute(statement).scalars().all()
@router.get("/users", response_model=list[UserPublic])
@admin
async def list_users(request: Request):
with Session(engine) as session:
statement = (
select(User)
.options(selectinload(User.minecraft_user))
.order_by(User.internal_username)
)
return session.execute(statement).scalars().all()
@router.get("/users/{internal_username}", response_model=UserPublic)
@admin
async def get_user(request: Request, internal_username: str):
with Session(engine) as session:
statement = (
select(User)
.where(User.internal_username == internal_username)
.options(selectinload(User.minecraft_user))
)
user = session.execute(statement).scalars().first()
if user is None:
raise HTTPException(status_code=404)
return user
@router.get("/users/by-uuid/{uuid}", response_model=UserPublic)
@admin
async def get_user_by_uuid(request: Request, uuid: str):
with Session(engine) as session:
statement = (
select(User)
.join(MinecraftUser)
.where(MinecraftUser.uuid == uuid)
.options(selectinload(User.minecraft_user))
)
user = session.execute(statement).scalars().first()
if user is None:
raise HTTPException(status_code=404)
return user
+5 -2
View File
@@ -8,7 +8,8 @@ from starlette.authentication import (
) )
from starlette.requests import HTTPConnection from starlette.requests import HTTPConnection
from starlette.responses import JSONResponse, Response from starlette.responses import JSONResponse, Response
from sqlmodel import Session, select from sqlalchemy.orm import selectinload, Session
from sqlalchemy import select
from loon.web.db import engine from loon.web.db import engine
from loon.web.users.models import User from loon.web.users.models import User
@@ -62,7 +63,9 @@ class JWTAuthBackend(AuthenticationBackend):
raise AuthenticationError("Invalid or expired token") raise AuthenticationError("Invalid or expired token")
with Session(engine) as session: with Session(engine) as session:
user = session.exec(select(User).where(User.id == user_id)).first() user = session.execute(
select(User).where(User.id == user_id).options(selectinload(User.minecraft_user))
).scalars().first()
if user is None: if user is None:
raise AuthenticationError("User not found") raise AuthenticationError("User not found")
+3 -2
View File
@@ -1,5 +1,6 @@
from fastapi import APIRouter, HTTPException, Request, status from fastapi import APIRouter, HTTPException, Request, status
from sqlmodel import Session, select from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web import limiter from loon.web import limiter
from loon.web.auth.middleware import authenticated from loon.web.auth.middleware import authenticated
@@ -17,7 +18,7 @@ router = APIRouter(prefix="/auth", tags=["auth"])
async def get_jwt(request: Request, body: JWTRequest) -> JWTResponse: async def get_jwt(request: Request, body: JWTRequest) -> JWTResponse:
with Session(engine) as session: with Session(engine) as session:
statement = select(User).where(User.internal_username == body.username) statement = select(User).where(User.internal_username == body.username)
user = session.exec(statement).first() user = session.execute(statement).scalars().first()
if user is None or user.id is None or not verify_password(body.password, user.password): if user is None or user.id is None or not verify_password(body.password, user.password):
raise HTTPException( raise HTTPException(
+6 -1
View File
@@ -1,8 +1,13 @@
from sqlmodel import SQLModel, create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase
from config import settings from config import settings
class Base(DeclarativeBase):
pass
def database_url(url: str | None = None) -> str: def database_url(url: str | None = None) -> str:
url = url or settings.database.url url = url or settings.database.url
for src, dst in ( for src, dst in (
View File
Whitespace-only changes.
+24
View File
@@ -0,0 +1,24 @@
from datetime import datetime, timezone
from sqlalchemy import DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from loon.web.db import Base
class LogType(Base):
__tablename__ = "logtype"
id: Mapped[int] = mapped_column(primary_key=True)
code: Mapped[str] = mapped_column(unique=True)
entries: Mapped[list["LogEntry"]] = relationship(back_populates="log_type")
class LogEntry(Base):
__tablename__ = "logentry"
id: Mapped[int] = mapped_column(primary_key=True)
log_type_id: Mapped[int] = mapped_column(ForeignKey("logtype.id"))
log_type: Mapped[LogType] = relationship(back_populates="entries")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
message: Mapped[str]
+10
View File
@@ -0,0 +1,10 @@
from datetime import datetime
from pydantic import BaseModel
class LogEntryResponse(BaseModel):
id: int
log_type: str
created_at: datetime
message: str
+32
View File
@@ -0,0 +1,32 @@
import asyncio
from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web.db import engine
from loon.web.logs.models import LogType, LogEntry
from loon.web.logs.types import LogTypeEnum
def player_tag(uuid: str, username: str) -> str:
return f"[player:{uuid}]{username}[/player]"
def _write_log(log_type: LogTypeEnum, message: str) -> None:
with Session(engine) as session:
statement = select(LogType).where(LogType.code == log_type.value)
log_type_object = session.execute(statement).scalars().first()
if not log_type_object:
log_type_object = LogType(code=log_type.value)
session.add(log_type_object)
session.commit()
session.refresh(log_type_object)
log_entry = LogEntry(log_type_id=log_type_object.id, message=message)
session.add(log_entry)
session.commit()
async def write_log(log_type: LogTypeEnum, message: str) -> None:
await asyncio.to_thread(_write_log, log_type, message)
+11
View File
@@ -0,0 +1,11 @@
from enum import Enum
class LogTypeEnum(Enum):
PLAYER_DEAD = "player_dead"
PLAYER_JOINED = "player_joined"
PLAYER_LEFT = "player_left"
PLAYER_KICKED = "player_kicked"
PLAYER_BANNED = "player_banned"
PLAYER_REGISTERED = "player_registered"
PLAYER_PASSWORD_CHANGED = "player_password_changed"
+28 -7
View File
@@ -1,9 +1,30 @@
from sqlmodel import SQLModel, Field from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from loon.web.db import Base
class User(SQLModel, table=True): class MinecraftUser(Base):
id: int = Field(default=None, primary_key=True) __tablename__ = "minecraftuser"
uuid: str = Field(unique=True)
internal_username: str = Field(unique=True) id: Mapped[int] = mapped_column(primary_key=True)
password: str uuid: Mapped[str] = mapped_column(unique=True)
admin: bool = Field(default=False) username: Mapped[str]
user: Mapped["User"] = relationship(back_populates="minecraft_user")
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
minecraft_user_id: Mapped[int] = mapped_column(ForeignKey("minecraftuser.id"), unique=True)
minecraft_user: Mapped[MinecraftUser] = relationship(back_populates="user")
internal_username: Mapped[str] = mapped_column(unique=True)
password: Mapped[str]
admin: Mapped[bool] = mapped_column(default=False)
@property
def uuid(self) -> str:
return self.minecraft_user.uuid
+5 -3
View File
@@ -3,15 +3,17 @@ from starlette.websockets import WebSocket, WebSocketDisconnect
from loon.web import get_mqtt_manager from loon.web import get_mqtt_manager
from loon.web.auth.middleware import authenticated from loon.web.auth.middleware import authenticated
from loon.web.users.models import User
from loon.web.users.schema import UserPublic
from loon.web.users.state import subscribe_player, get_user_messages, unsubscribe_player from loon.web.users.state import subscribe_player, get_user_messages, unsubscribe_player
router = APIRouter(prefix="/users", tags=["users"]) router = APIRouter(prefix="/users", tags=["users"])
@router.get("/me") @router.get("/me", response_model=UserPublic)
@authenticated @authenticated
async def me(request: Request): async def me(request: Request) -> User:
return request.user.user.model_dump(exclude={"password", "id"}) return request.user.user
@router.post("/inventory/request") @router.post("/inventory/request")
@authenticated @authenticated
+16
View File
@@ -0,0 +1,16 @@
from pydantic import BaseModel, ConfigDict
class MinecraftUserPublic(BaseModel):
model_config = ConfigDict(from_attributes=True)
uuid: str
username: str
class UserPublic(BaseModel):
model_config = ConfigDict(from_attributes=True)
internal_username: str
admin: bool
minecraft_user: MinecraftUserPublic
+44
View File
@@ -0,0 +1,44 @@
import asyncio
from sqlalchemy import select
from sqlalchemy.orm import Session
from loon.web.db import engine
from loon.web.users.models import MinecraftUser
def _sync_minecraft_users(players: list[dict]) -> list[MinecraftUser]:
players_by_uuid = {player["uuid"]: player["username"] for player in players}
with Session(engine) as session:
existing = session.execute(
select(MinecraftUser).where(MinecraftUser.uuid.in_(players_by_uuid))
).scalars().all()
existing_by_uuid = {minecraft_user.uuid: minecraft_user for minecraft_user in existing}
new_users = []
for uuid, username in players_by_uuid.items():
minecraft_user = existing_by_uuid.get(uuid)
if minecraft_user is None:
minecraft_user = MinecraftUser(uuid=uuid, username=username)
new_users.append(minecraft_user)
session.add(minecraft_user)
elif minecraft_user.username != username:
minecraft_user.username = username
session.add(minecraft_user)
session.commit()
minecraft_users = [*existing, *new_users]
for minecraft_user in minecraft_users:
session.refresh(minecraft_user)
return minecraft_users
async def sync_minecraft_users(players: list[dict]) -> list[MinecraftUser]:
if not players:
return []
return await asyncio.to_thread(_sync_minecraft_users, players)
-1
View File
@@ -55,7 +55,6 @@ dependencies = [
"shellingham==1.5.4", "shellingham==1.5.4",
"slowapi>=0.1.10", "slowapi>=0.1.10",
"sqlalchemy==2.0.51", "sqlalchemy==2.0.51",
"sqlmodel==0.0.39",
"starlette==1.3.1", "starlette==1.3.1",
"typer==0.27.0", "typer==0.27.0",
"typing-extensions==4.16.0", "typing-extensions==4.16.0",
Generated
-16
View File
@@ -516,7 +516,6 @@ dependencies = [
{ name = "shellingham" }, { name = "shellingham" },
{ name = "slowapi" }, { name = "slowapi" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "sqlmodel" },
{ name = "starlette" }, { name = "starlette" },
{ name = "typer" }, { name = "typer" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
@@ -580,7 +579,6 @@ requires-dist = [
{ name = "shellingham", specifier = "==1.5.4" }, { name = "shellingham", specifier = "==1.5.4" },
{ name = "slowapi", specifier = ">=0.1.10" }, { name = "slowapi", specifier = ">=0.1.10" },
{ name = "sqlalchemy", specifier = "==2.0.51" }, { name = "sqlalchemy", specifier = "==2.0.51" },
{ name = "sqlmodel", specifier = "==0.0.39" },
{ name = "starlette", specifier = "==1.3.1" }, { name = "starlette", specifier = "==1.3.1" },
{ name = "typer", specifier = "==0.27.0" }, { name = "typer", specifier = "==0.27.0" },
{ name = "typing-extensions", specifier = "==4.16.0" }, { name = "typing-extensions", specifier = "==4.16.0" },
@@ -1022,20 +1020,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" },
] ]
[[package]]
name = "sqlmodel"
version = "0.0.39"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "sqlalchemy" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/68/ee/22a0559283c3cf6048678e787ed5d4959dcd00dedd8ba4567eeae684eeb1/sqlmodel-0.0.39.tar.gz", hash = "sha256:23d8e50a8d8ee936032ed79c55023a5d618dd6bc3c510bbf4909d1a7a605a570", size = 91057, upload-time = "2026-06-25T13:01:38.475Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/7d/b9813a582d4eb310be35e1fc7dfaae71207d7b62e9e53be314ebd251b53b/sqlmodel-0.0.39-py3-none-any.whl", hash = "sha256:90ebe92ce5cc11d7fff8dc7cb594790a102333c8fe7c14865254f6fc5c939795", size = 29680, upload-time = "2026-06-25T13:01:37.494Z" },
]
[[package]] [[package]]
name = "starlette" name = "starlette"
version = "1.3.1" version = "1.3.1"