mirror of
https://github.com/nihilvux/bancho.py.git
synced 2025-09-17 11:07:54 -07:00
Add files via upload
This commit is contained in:
314
app/objects/collections.py
Normal file
314
app/objects/collections.py
Normal file
@@ -0,0 +1,314 @@
|
||||
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",
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user