mirror of
https://github.com/nihilvux/bancho.py.git
synced 2025-09-16 18:48:38 -07:00
315 lines
9.6 KiB
Python
315 lines
9.6 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable
|
|
from collections.abc import Iterator
|
|
from collections.abc import Sequence
|
|
from typing import Any
|
|
|
|
import databases.core
|
|
|
|
import app.settings
|
|
import app.state
|
|
import app.utils
|
|
from app.constants.privileges import ClanPrivileges
|
|
from app.constants.privileges import Privileges
|
|
from app.logging import Ansi
|
|
from app.logging import log
|
|
from app.objects.channel import Channel
|
|
from app.objects.match import Match
|
|
from app.objects.player import Player
|
|
from app.repositories import channels as channels_repo
|
|
from app.repositories import clans as clans_repo
|
|
from app.repositories import users as users_repo
|
|
from app.utils import make_safe_name
|
|
|
|
|
|
class Channels(list[Channel]):
|
|
"""The currently active chat channels on the server."""
|
|
|
|
def __iter__(self) -> Iterator[Channel]:
|
|
return super().__iter__()
|
|
|
|
def __contains__(self, o: object) -> bool:
|
|
"""Check whether internal list contains `o`."""
|
|
# Allow string to be passed to compare vs. name.
|
|
if isinstance(o, str):
|
|
return o in (chan.name for chan in self)
|
|
else:
|
|
return super().__contains__(o)
|
|
|
|
def __repr__(self) -> str:
|
|
# XXX: we use the "real" name, aka
|
|
# #multi_1 instead of #multiplayer
|
|
# #spect_1 instead of #spectator.
|
|
return f'[{", ".join(c._name for c in self)}]'
|
|
|
|
def get_by_name(self, name: str) -> Channel | None:
|
|
"""Get a channel from the list by `name`."""
|
|
for channel in self:
|
|
if channel._name == name:
|
|
return channel
|
|
|
|
return None
|
|
|
|
def append(self, channel: Channel) -> None:
|
|
"""Append `channel` to the list."""
|
|
super().append(channel)
|
|
|
|
if app.settings.DEBUG:
|
|
log(f"{channel} added to channels list.")
|
|
|
|
def extend(self, channels: Iterable[Channel]) -> None:
|
|
"""Extend the list with `channels`."""
|
|
super().extend(channels)
|
|
|
|
if app.settings.DEBUG:
|
|
log(f"{channels} added to channels list.")
|
|
|
|
def remove(self, channel: Channel) -> None:
|
|
"""Remove `channel` from the list."""
|
|
super().remove(channel)
|
|
|
|
if app.settings.DEBUG:
|
|
log(f"{channel} removed from channels list.")
|
|
|
|
async def prepare(self) -> None:
|
|
"""Fetch data from sql & return; preparing to run the server."""
|
|
log("Fetching channels from sql.", Ansi.LCYAN)
|
|
for row in await channels_repo.fetch_many():
|
|
self.append(
|
|
Channel(
|
|
name=row["name"],
|
|
topic=row["topic"],
|
|
read_priv=Privileges(row["read_priv"]),
|
|
write_priv=Privileges(row["write_priv"]),
|
|
auto_join=row["auto_join"] == 1,
|
|
),
|
|
)
|
|
|
|
|
|
class Matches(list[Match | None]):
|
|
"""The currently active multiplayer matches on the server."""
|
|
|
|
def __init__(self) -> None:
|
|
MAX_MATCHES = 64 # TODO: refactor this out of existence
|
|
super().__init__([None] * MAX_MATCHES)
|
|
|
|
def __iter__(self) -> Iterator[Match | None]:
|
|
return super().__iter__()
|
|
|
|
def __repr__(self) -> str:
|
|
return f'[{", ".join(match.name for match in self if match)}]'
|
|
|
|
def get_free(self) -> int | None:
|
|
"""Return the first free match id from `self`."""
|
|
for idx, match in enumerate(self):
|
|
if match is None:
|
|
return idx
|
|
|
|
return None
|
|
|
|
def remove(self, match: Match | None) -> None:
|
|
"""Remove `match` from the list."""
|
|
for i, _m in enumerate(self):
|
|
if match is _m:
|
|
self[i] = None
|
|
break
|
|
|
|
if app.settings.DEBUG:
|
|
log(f"{match} removed from matches list.")
|
|
|
|
|
|
class Players(list[Player]):
|
|
"""The currently active players on the server."""
|
|
|
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def __iter__(self) -> Iterator[Player]:
|
|
return super().__iter__()
|
|
|
|
def __contains__(self, player: object) -> bool:
|
|
# allow us to either pass in the player
|
|
# obj, or the player name as a string.
|
|
if isinstance(player, str):
|
|
return player in (player.name for player in self)
|
|
else:
|
|
return super().__contains__(player)
|
|
|
|
def __repr__(self) -> str:
|
|
return f'[{", ".join(map(repr, self))}]'
|
|
|
|
@property
|
|
def ids(self) -> set[int]:
|
|
"""Return a set of the current ids in the list."""
|
|
return {p.id for p in self}
|
|
|
|
@property
|
|
def staff(self) -> set[Player]:
|
|
"""Return a set of the current staff online."""
|
|
return {p for p in self if p.priv & Privileges.STAFF}
|
|
|
|
@property
|
|
def restricted(self) -> set[Player]:
|
|
"""Return a set of the current restricted players."""
|
|
return {p for p in self if not p.priv & Privileges.UNRESTRICTED}
|
|
|
|
@property
|
|
def unrestricted(self) -> set[Player]:
|
|
"""Return a set of the current unrestricted players."""
|
|
return {p for p in self if p.priv & Privileges.UNRESTRICTED}
|
|
|
|
def enqueue(self, data: bytes, immune: Sequence[Player] = []) -> None:
|
|
"""Enqueue `data` to all players, except for those in `immune`."""
|
|
for player in self:
|
|
if player not in immune:
|
|
player.enqueue(data)
|
|
|
|
def get(
|
|
self,
|
|
token: str | None = None,
|
|
id: int | None = None,
|
|
name: str | None = None,
|
|
) -> Player | None:
|
|
"""Get a player by token, id, or name from cache."""
|
|
for player in self:
|
|
if token is not None:
|
|
if player.token == token:
|
|
return player
|
|
elif id is not None:
|
|
if player.id == id:
|
|
return player
|
|
elif name is not None:
|
|
if player.safe_name == make_safe_name(name):
|
|
return player
|
|
|
|
return None
|
|
|
|
async def get_sql(
|
|
self,
|
|
id: int | None = None,
|
|
name: str | None = None,
|
|
) -> Player | None:
|
|
"""Get a player by token, id, or name from sql."""
|
|
# try to get from sql.
|
|
player = await users_repo.fetch_one(
|
|
id=id,
|
|
name=name,
|
|
fetch_all_fields=True,
|
|
)
|
|
if player is None:
|
|
return None
|
|
|
|
clan_id: int | None = None
|
|
clan_priv: ClanPrivileges | None = None
|
|
if player["clan_id"] != 0:
|
|
clan_id = player["clan_id"]
|
|
clan_priv = ClanPrivileges(player["clan_priv"])
|
|
|
|
return Player(
|
|
id=player["id"],
|
|
name=player["name"],
|
|
priv=Privileges(player["priv"]),
|
|
pw_bcrypt=player["pw_bcrypt"].encode(),
|
|
token=Player.generate_token(),
|
|
clan_id=clan_id,
|
|
clan_priv=clan_priv,
|
|
geoloc={
|
|
"latitude": 0.0,
|
|
"longitude": 0.0,
|
|
"country": {
|
|
"acronym": player["country"],
|
|
"numeric": app.state.services.country_codes[player["country"]],
|
|
},
|
|
},
|
|
silence_end=player["silence_end"],
|
|
donor_end=player["donor_end"],
|
|
api_key=player["api_key"],
|
|
)
|
|
|
|
async def from_cache_or_sql(
|
|
self,
|
|
id: int | None = None,
|
|
name: str | None = None,
|
|
) -> Player | None:
|
|
"""Try to get player from cache, or sql as fallback."""
|
|
player = self.get(id=id, name=name)
|
|
if player is not None:
|
|
return player
|
|
player = await self.get_sql(id=id, name=name)
|
|
if player is not None:
|
|
return player
|
|
|
|
return None
|
|
|
|
async def from_login(
|
|
self,
|
|
name: str,
|
|
pw_md5: str,
|
|
sql: bool = False,
|
|
) -> Player | None:
|
|
"""Return a player with a given name & pw_md5, from cache or sql."""
|
|
player = self.get(name=name)
|
|
if not player:
|
|
if not sql:
|
|
return None
|
|
|
|
player = await self.get_sql(name=name)
|
|
if not player:
|
|
return None
|
|
|
|
assert player.pw_bcrypt is not None
|
|
|
|
if app.state.cache.bcrypt[player.pw_bcrypt] == pw_md5.encode():
|
|
return player
|
|
|
|
return None
|
|
|
|
def append(self, player: Player) -> None:
|
|
"""Append `p` to the list."""
|
|
if player in self:
|
|
if app.settings.DEBUG:
|
|
log(f"{player} double-added to global player list?")
|
|
return
|
|
|
|
super().append(player)
|
|
|
|
def remove(self, player: Player) -> None:
|
|
"""Remove `p` from the list."""
|
|
if player not in self:
|
|
if app.settings.DEBUG:
|
|
log(f"{player} removed from player list when not online?")
|
|
return
|
|
|
|
super().remove(player)
|
|
|
|
|
|
async def initialize_ram_caches() -> None:
|
|
"""Setup & cache the global collections before listening for connections."""
|
|
# fetch channels, clans and pools from db
|
|
await app.state.sessions.channels.prepare()
|
|
|
|
bot = await users_repo.fetch_one(id=1)
|
|
if bot is None:
|
|
raise RuntimeError("Bot account not found in database.")
|
|
|
|
# create bot & add it to online players
|
|
app.state.sessions.bot = Player(
|
|
id=1,
|
|
name=bot["name"],
|
|
priv=Privileges.UNRESTRICTED,
|
|
pw_bcrypt=None,
|
|
token=Player.generate_token(),
|
|
login_time=float(0x7FFFFFFF), # (never auto-dc)
|
|
is_bot_client=True,
|
|
)
|
|
app.state.sessions.players.append(app.state.sessions.bot)
|
|
|
|
# static api keys
|
|
app.state.sessions.api_keys = {
|
|
row["api_key"]: row["id"]
|
|
for row in await app.state.services.database.fetch_all(
|
|
"SELECT id, api_key FROM users WHERE api_key IS NOT NULL",
|
|
)
|
|
}
|