Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move user sessions to redis; deprecate player object & players collection #622

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,419 changes: 982 additions & 437 deletions app/api/domains/cho.py

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion app/bg_loops.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import app.packets
import app.settings
import app.state
from app import builtin_bot
from app.constants.privileges import Privileges
from app.logging import Ansi
from app.logging import log
Expand Down Expand Up @@ -86,4 +87,4 @@ async def _update_bot_status(interval: int) -> None:
"""Re roll the bot status, every `interval`."""
while True:
await asyncio.sleep(interval)
app.packets.bot_stats.cache_clear()
builtin_bot.bot_user_stats.cache_clear()
74 changes: 74 additions & 0 deletions app/builtin_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from __future__ import annotations

import random
from functools import cache

from app import packets
from app.constants.privileges import Privileges
from app.constants.privileges import get_client_privileges

BOT_USER_ID = 1
BOT_USER_NAME = "Aika"
BOT_PRIVILEGES = (
Privileges.UNRESTRICTED
| Privileges.DONATOR
| Privileges.MODERATOR
| Privileges.ADMINISTRATOR
| Privileges.DEVELOPER
)

BOT_USER_STATUSES = (
(3, "the source code.."), # editing
(6, "geohot livestreams.."), # watching
(6, "asottile tutorials.."), # watching
(6, "over the server.."), # watching
(8, "out new features.."), # testing
(9, "a pull request.."), # submitting
)
# lat/long off-screen for in-game world map
BOT_LATITUDE = 1234.0
BOT_LONGITUDE = 4321.0
BOT_UTC_OFFSET = -5 # America/Toronto
BOT_COUNTRY_CODE = 256 # Satellite Provider


@cache
def bot_user_stats() -> bytes:
"""\
Cached user stats packet for the bot user.

NOTE: the cache for this is cleared every 5mins by
`app.bg_loops._update_bot_status`.
"""
status_id, status_txt = random.choice(BOT_USER_STATUSES)
return packets.write(
packets.ServerPackets.USER_STATS,
(BOT_USER_ID, packets.osuTypes.i32), # id
(status_id, packets.osuTypes.u8), # action
(status_txt, packets.osuTypes.string), # info_text
("", packets.osuTypes.string), # map_md5
(0, packets.osuTypes.i32), # mods
(0, packets.osuTypes.u8), # mode
(0, packets.osuTypes.i32), # map_id
(0, packets.osuTypes.i64), # rscore
(0.0, packets.osuTypes.f32), # acc
(0, packets.osuTypes.i32), # plays
(0, packets.osuTypes.i64), # tscore
(0, packets.osuTypes.i32), # rank
(0, packets.osuTypes.i16), # pp
)


@cache
def bot_user_presence() -> bytes:
return packets.write(
packets.ServerPackets.USER_PRESENCE,
(BOT_USER_ID, packets.osuTypes.i32),
(BOT_USER_NAME, packets.osuTypes.string),
(BOT_UTC_OFFSET + 24, packets.osuTypes.u8),
(BOT_COUNTRY_CODE, packets.osuTypes.u8),
(get_client_privileges(BOT_PRIVILEGES), packets.osuTypes.u8),
(BOT_LATITUDE, packets.osuTypes.f32),
(BOT_LONGITUDE, packets.osuTypes.f32),
(0, packets.osuTypes.i32),
)
28 changes: 9 additions & 19 deletions app/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ async def changename(ctx: Context) -> str | None:
return "Username already taken by another player."

# all checks passed, update their name
await users_repo.update(ctx.player.id, name=name)
await users_repo.partial_update(ctx.player.id, name=name)

ctx.player.enqueue(
app.packets.notification(f"Your username has been changed to {name}!"),
Expand Down Expand Up @@ -564,7 +564,7 @@ async def apikey(ctx: Context) -> str | None:
# generate new token
ctx.player.api_key = str(uuid.uuid4())

await users_repo.update(ctx.player.id, api_key=ctx.player.api_key)
await users_repo.partial_update(ctx.player.id, api_key=ctx.player.api_key)
app.state.sessions.api_keys[ctx.player.api_key] = ctx.player.id

return f"API key generated. Copy your api key from (this url)[http://{ctx.player.api_key}]."
Expand Down Expand Up @@ -1033,16 +1033,6 @@ async def shutdown(ctx: Context) -> str | None | NoReturn:
"""


@command(Privileges.DEVELOPER)
async def stealth(ctx: Context) -> str | None:
"""Toggle the developer's stealth, allowing them to be hidden."""
# NOTE: this command is a large work in progress and currently
# half works; eventually it will be moved to the Admin level.
ctx.player.stealth = not ctx.player.stealth

return f'Stealth {"enabled" if ctx.player.stealth else "disabled"}.'


@command(Privileges.DEVELOPER)
async def recalc(ctx: Context) -> str | None:
"""Recalculate pp for a given map, or all maps."""
Expand Down Expand Up @@ -1349,7 +1339,7 @@ async def wrapper(ctx: Context) -> str | None:
# player not in a match
return None

if ctx.recipient is not match.chat:
if ctx.recipient is not match.chat_channel_id:
# message not in match channel
return None

Expand Down Expand Up @@ -1423,15 +1413,15 @@ def _start() -> None:
# make sure player didn't leave the
# match since queueing this start lol...
if ctx.player not in {slot.player for slot in match.slots}:
match.chat.send_bot("Player left match? (cancelled)")
match.chat_channel_id.send_bot("Player left match? (cancelled)")
return

match.start()
match.chat.send_bot("Starting match.")
match.chat_channel_id.send_bot("Starting match.")

def _alert_start(t: int) -> None:
"""Alert the match of the impending start."""
match.chat.send_bot(f"Match starting in {t} seconds.")
match.chat_channel_id.send_bot(f"Match starting in {t} seconds.")

# add timers to our match object,
# so we can cancel them if needed.
Expand Down Expand Up @@ -2322,7 +2312,7 @@ async def clan_create(ctx: Context) -> str | None:
ctx.player.clan_id = new_clan["id"]
ctx.player.clan_priv = ClanPrivileges.Owner

await users_repo.update(
await users_repo.partial_update(
ctx.player.id,
clan_id=new_clan["id"],
clan_priv=ClanPrivileges.Owner,
Expand Down Expand Up @@ -2366,7 +2356,7 @@ async def clan_disband(ctx: Context) -> str | None:
for clan_member in await users_repo.fetch_many(clan_id=clan["id"])
]
for member_id in clan_member_ids:
await users_repo.update(member_id, clan_id=0, clan_priv=0)
await users_repo.partial_update(member_id, clan_id=0, clan_priv=0)

member = app.state.sessions.players.get(id=member_id)
if member:
Expand Down Expand Up @@ -2419,7 +2409,7 @@ async def clan_leave(ctx: Context) -> str | None:

clan_members = await users_repo.fetch_many(clan_id=clan["id"])

await users_repo.update(ctx.player.id, clan_id=0, clan_priv=0)
await users_repo.partial_update(ctx.player.id, clan_id=0, clan_priv=0)
ctx.player.clan_id = None
ctx.player.clan_priv = None

Expand Down
43 changes: 43 additions & 0 deletions app/constants/grades.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations

import functools
from enum import IntEnum
from enum import unique


@unique
class Grade(IntEnum):
# NOTE: these are implemented in the opposite order
# as osu! to make more sense with <> operators.
N = 0
F = 1
D = 2
C = 3
B = 4
A = 5
S = 6 # S
SH = 7 # HD S
X = 8 # SS
XH = 9 # HD SS

@classmethod
@functools.cache
def from_str(cls, s: str) -> Grade:
return {
"xh": Grade.XH,
"x": Grade.X,
"sh": Grade.SH,
"s": Grade.S,
"a": Grade.A,
"b": Grade.B,
"c": Grade.C,
"d": Grade.D,
"f": Grade.F,
"n": Grade.N,
}[s.lower()]

def __format__(self, format_spec: str) -> str:
if format_spec == "stats_column":
return f"{self.name.lower()}_count"
else:
raise ValueError(f"Invalid format specifier {format_spec}")
48 changes: 48 additions & 0 deletions app/constants/multiplayer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

from enum import IntEnum
from enum import unique

from app.utils import escape_enum
from app.utils import pymysql_encode


@unique
@pymysql_encode(escape_enum)
class MatchWinConditions(IntEnum):
SCORE = 0
ACCURACY = 1
COMBO = 2
SCORE_V2 = 3


@unique
@pymysql_encode(escape_enum)
class MatchTeamTypes(IntEnum):
HEAD_TO_HEAD = 0
TAG_CO_OP = 1
TEAM_VS = 2
TAG_TEAM_VS = 3


@unique
@pymysql_encode(escape_enum)
class MatchTeams(IntEnum):
NEUTRAL = 0
BLUE = 1
RED = 2


@unique
@pymysql_encode(escape_enum)
class SlotStatus(IntEnum):
OPEN = 1
LOCKED = 2
NOT_READY = 4
READY = 8
NO_MAP = 16
PLAYING = 32
COMPLETE = 64
QUIT = 128

# HAS_PLAYER = NOT_READY | READY | NO_MAP | PLAYING | COMPLETE
62 changes: 62 additions & 0 deletions app/constants/osu_client_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from __future__ import annotations

from datetime import date
from enum import StrEnum
from functools import cached_property

from app._typing import IPAddress


class OsuStream(StrEnum):
STABLE = "stable"
BETA = "beta"
CUTTINGEDGE = "cuttingedge"
TOURNEY = "tourney"
DEV = "dev"


class OsuVersion:
# b20200201.2cuttingedge
# date = 2020/02/01
# revision = 2
# stream = cuttingedge
def __init__(
self,
date: date,
revision: int | None, # TODO: should this be optional?
stream: OsuStream,
) -> None:
self.date = date
self.revision = revision
self.stream = stream


class ClientDetails:
def __init__(
self,
osu_version: OsuVersion,
osu_path_md5: str,
adapters_md5: str,
uninstall_md5: str,
disk_signature_md5: str,
adapters: list[str],
ip: IPAddress,
) -> None:
self.osu_version = osu_version
self.osu_path_md5 = osu_path_md5
self.adapters_md5 = adapters_md5
self.uninstall_md5 = uninstall_md5
self.disk_signature_md5 = disk_signature_md5

self.adapters = adapters
self.ip = ip

@cached_property
def client_hash(self) -> str:
return (
# NOTE the extra '.' and ':' appended to ends
f"{self.osu_path_md5}:{'.'.join(self.adapters)}."
f":{self.adapters_md5}:{self.uninstall_md5}:{self.disk_signature_md5}:"
)

# TODO: __str__ to pack like osu! hashes?
18 changes: 18 additions & 0 deletions app/constants/privileges.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from enum import IntEnum
from enum import IntFlag
from enum import unique
from functools import cache

from app.utils import escape_enum
from app.utils import pymysql_encode
Expand Down Expand Up @@ -59,3 +60,20 @@ class ClanPrivileges(IntEnum):
Member = 1
Officer = 2
Owner = 3


@cache
def get_client_privileges(server_privileges: Privileges) -> ClientPrivileges:
"""The player's privileges according to the client."""
ret = ClientPrivileges(0)
if server_privileges & Privileges.UNRESTRICTED:
ret |= ClientPrivileges.PLAYER
if server_privileges & Privileges.DONATOR:
ret |= ClientPrivileges.SUPPORTER
if server_privileges & Privileges.MODERATOR:
ret |= ClientPrivileges.MODERATOR
if server_privileges & Privileges.ADMINISTRATOR:
ret |= ClientPrivileges.DEVELOPER
if server_privileges & Privileges.DEVELOPER:
ret |= ClientPrivileges.OWNER
return ret
16 changes: 16 additions & 0 deletions app/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from __future__ import annotations

from enum import StrEnum

from pydantic import BaseModel


class ErrorCode(StrEnum):
INVALID_REQUEST = "invalid_request"
INTERNAL_SERVER_ERROR = "internal_server_error"
RESOURCE_NOT_FOUND = "resource_not_found"


class Error(BaseModel):
user_feedback: str
error_code: ErrorCode
Loading
Loading