mirror of
https://github.com/kuitoi/kuitoi-Server.git
synced 2025-08-17 08:15:42 +00:00
Compare commits
7 Commits
c8fea133ba
...
9ae200d48a
Author | SHA1 | Date | |
---|---|---|---|
9ae200d48a | |||
3b5324d115 | |||
f181a82e0e | |||
b271c80e39 | |||
ef9a55c407 | |||
98b4878339 | |||
b345588c02 |
@ -63,7 +63,7 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
|
||||
- [ ] Client (Player) class
|
||||
- [ ] Lua part: (Original BeamMP compatibility)
|
||||
- [x] Load Lua plugins
|
||||
- [ ] MP Class
|
||||
- [x] MP Class
|
||||
- [ ] Util class
|
||||
- [ ] FS class
|
||||
- [x] MultiLanguage (i18n support)
|
||||
|
@ -33,9 +33,16 @@ class Client:
|
||||
self.roles = None
|
||||
self._guest = True
|
||||
self._ready = False
|
||||
self._identifiers = []
|
||||
self._cars = [None] * 21 # Max 20 cars per player + 1 snowman
|
||||
self._snowman = {"id": -1, "packet": ""}
|
||||
self._connect_time = 0
|
||||
self._last_position = {}
|
||||
|
||||
ev.register_event("onServerStopped", self.__gracefully_kick)
|
||||
|
||||
async def __gracefully_kick(self, _):
|
||||
await self.kick("Server shutdown!")
|
||||
|
||||
@property
|
||||
def _writer(self):
|
||||
@ -65,10 +72,18 @@ class Client:
|
||||
def ready(self):
|
||||
return self._ready
|
||||
|
||||
@property
|
||||
def identifiers(self):
|
||||
return self._identifiers
|
||||
|
||||
@property
|
||||
def cars(self):
|
||||
return self._cars
|
||||
|
||||
@property
|
||||
def last_position(self):
|
||||
return self._last_position
|
||||
|
||||
def _update_logger(self):
|
||||
self._log = utils.get_logger(f"{self.nick}:{self.cid}")
|
||||
self.log.debug(f"Update logger")
|
||||
@ -94,7 +109,7 @@ class Client:
|
||||
self.__alive = False
|
||||
|
||||
async def send_message(self, message, to_all=True):
|
||||
pass
|
||||
await self._send(f"C:{message}", to_all=to_all)
|
||||
|
||||
async def send_event(self, event_name, event_data):
|
||||
pass
|
||||
@ -373,7 +388,8 @@ class Client:
|
||||
"json": car_json,
|
||||
"json_ok": bool(car_json),
|
||||
"snowman": snowman,
|
||||
"over_spawn": (snowman and allow_snowman) or over_spawn
|
||||
"over_spawn": (snowman and allow_snowman) or over_spawn,
|
||||
"pos": {}
|
||||
}
|
||||
await self._send(pkt, to_all=True, to_self=True)
|
||||
else:
|
||||
@ -381,8 +397,13 @@ class Client:
|
||||
des = f"Od:{self.cid}-{car_id}"
|
||||
await self._send(des)
|
||||
|
||||
async def _delete_car(self, raw_data):
|
||||
cid, car_id = self._get_cid_vid(raw_data)
|
||||
async def _delete_car(self, raw_data=None, car_id=None):
|
||||
|
||||
if not car_id and raw_data:
|
||||
cid, car_id = self._get_cid_vid(raw_data)
|
||||
else:
|
||||
cid = self.cid
|
||||
raw_data = f"Od:{self.cid}-{car_id}"
|
||||
|
||||
if car_id != -1 and self.cars[car_id]:
|
||||
|
||||
@ -515,6 +536,7 @@ class Client:
|
||||
self.log.debug("Tried to send an empty event, ignoring")
|
||||
return
|
||||
to_ev = {"message": msg, "player": self}
|
||||
ev.call_lua_event("onChatMessage", self.cid, self.nick, msg)
|
||||
ev_data_list = ev.call_event("onChatReceive", **to_ev)
|
||||
d2 = await ev.call_async_event("onChatReceive", **to_ev)
|
||||
ev_data_list.extend(d2)
|
||||
|
@ -33,9 +33,11 @@ class Client:
|
||||
self._guest = True
|
||||
self.__alive = True
|
||||
self._ready = False
|
||||
self._identifiers = []
|
||||
self._cars: List[Optional[Dict[str, int]]] = []
|
||||
self._snowman: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
|
||||
|
||||
self._last_position = {}
|
||||
async def __gracefully_kick(self): ...
|
||||
@property
|
||||
def _writer(self) -> StreamWriter: ...
|
||||
@property
|
||||
@ -51,7 +53,11 @@ class Client:
|
||||
@property
|
||||
def ready(self) -> bool: ...
|
||||
@property
|
||||
def identifiers(self) -> list: ...
|
||||
@property
|
||||
def cars(self) -> List[dict | None]: ...
|
||||
@property
|
||||
def last_position(self): ...
|
||||
def is_disconnected(self) -> bool: ...
|
||||
async def kick(self, reason: str) -> None: ...
|
||||
async def send_message(self, message: str | bytes, to_all: bool = True) -> None:...
|
||||
@ -62,7 +68,7 @@ class Client:
|
||||
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str, sl: float) -> None: ...
|
||||
async def _get_cid_vid(self, s: str) -> Tuple[int, int]: ...
|
||||
async def _spawn_car(self, data: str) -> None: ...
|
||||
async def _delete_car(self, raw_data: str) -> None: ...
|
||||
async def _delete_car(self, raw_data: str = None, car_id : int =None) -> None: ...
|
||||
async def _edit_car(self, raw_data: str, data: str) -> None: ...
|
||||
async def _reset_car(self, raw_data: str) -> None: ...
|
||||
async def _handle_car_codes(self, data: str) -> None: ...
|
||||
|
@ -47,12 +47,12 @@ class Core:
|
||||
self.BeamMP_version = "3.1.1" # 20.07.2023
|
||||
|
||||
ev.register_event("_get_BeamMP_version", lambda x: tuple([int(i) for i in self.BeamMP_version.split(".")]))
|
||||
ev.register_event("_get_player", self.get_client)
|
||||
ev.register_event("_get_player", lambda x: self.get_client(**x['kwargs']))
|
||||
|
||||
def get_client(self, cid=None, nick=None, from_ev=None):
|
||||
if from_ev is not None:
|
||||
return self.get_client(*from_ev['args'], **from_ev['kwargs'])
|
||||
def get_client(self, cid=None, nick=None):
|
||||
if cid is not None:
|
||||
if cid == -1:
|
||||
return [i for i in self.clients if i is not None]
|
||||
return self.clients_by_id.get(cid)
|
||||
if nick:
|
||||
return self.clients_by_nick.get(nick)
|
||||
|
@ -56,6 +56,7 @@ class TCPServer:
|
||||
client.nick = res["username"]
|
||||
client.roles = res["roles"]
|
||||
client._guest = res["guest"]
|
||||
client._identifiers = {k: v for s in res["identifiers"] for k, v in [s.split(':')]}
|
||||
# noinspection PyProtectedMember
|
||||
client._update_logger()
|
||||
except Exception as e:
|
||||
|
@ -5,6 +5,7 @@
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from core import utils
|
||||
|
||||
@ -22,12 +23,14 @@ class UDPServer(asyncio.DatagramTransport):
|
||||
self.port = port
|
||||
self.run = False
|
||||
|
||||
def connection_made(self, transport): ...
|
||||
def connection_made(self, transport):
|
||||
...
|
||||
|
||||
async def handle_datagram(self, data, addr):
|
||||
try:
|
||||
cid = data[0] - 1
|
||||
code = data[2:3].decode()
|
||||
data = data[2:].decode()
|
||||
|
||||
client = self.Core.get_client(cid=cid)
|
||||
if client:
|
||||
@ -39,9 +42,19 @@ class UDPServer(asyncio.DatagramTransport):
|
||||
if client._udp_sock != (self.transport, addr):
|
||||
client._udp_sock = (self.transport, addr)
|
||||
self.log.debug(f"Set UDP Sock for CID: {cid}")
|
||||
ev.call_event("onChangePosition")
|
||||
if client:
|
||||
await client._send(data[2:], to_all=True, to_self=False, to_udp=True)
|
||||
ev.call_event("onChangePosition", data=data)
|
||||
sub = data.find("{", 1)
|
||||
last_pos_data = data[sub:]
|
||||
try:
|
||||
last_pos = json.loads(last_pos_data)
|
||||
client._last_position = last_pos
|
||||
_, car_id = client._get_cid_vid(data)
|
||||
client._cars[car_id]['pos'] = last_pos
|
||||
except Exception as e:
|
||||
self.log.debug(f"Cannot parse position packet: {e}")
|
||||
self.log.debug(f"data: {data}, sup: {sub}")
|
||||
self.log.debug(f"last_pos_data: {last_pos_data}")
|
||||
await client._send(data, to_all=True, to_self=False, to_udp=True)
|
||||
case _:
|
||||
self.log.debug(f"[{cid}] Unknown code: {code}")
|
||||
else:
|
||||
|
@ -46,12 +46,37 @@ class EventsSystem:
|
||||
"onServerStopped": []
|
||||
}
|
||||
|
||||
self.__lua_events = {
|
||||
"onInit": [], # onServerStarted
|
||||
"onShutdown": [], # onServerStopped
|
||||
"onPlayerAuth": [], # onPlayerAuthenticated
|
||||
"onPlayerConnecting": [], # TODO lua onPlayerConnecting
|
||||
"onPlayerJoining": [], # TODO lua onPlayerJoining
|
||||
"onPlayerJoin": [], # onPlayerJoin
|
||||
"onPlayerDisconnect": [], # TODO lua onPlayerDisconnect
|
||||
"onChatMessage": [], # onChatReceive
|
||||
"onVehicleSpawn": [], # "onCarSpawn
|
||||
"onVehicleEdited": [], # onCarEdited
|
||||
"onVehicleDeleted": [], # onCarDelete
|
||||
"onVehicleReset": [], # onCarReset
|
||||
"onFileChanged": [], # TODO lua onFileChanged
|
||||
}
|
||||
|
||||
def builtins_hook(self):
|
||||
self.log.debug("used builtins_hook")
|
||||
builtins.ev = self
|
||||
|
||||
def register_event(self, event_name, event_func, async_event=False):
|
||||
self.log.debug(f"register_event({event_name}, {event_func}):")
|
||||
def register_event(self, event_name, event_func, async_event=False, lua=None):
|
||||
self.log.debug(f"register_event(event_name='{event_name}', event_func='{event_func}', "
|
||||
f"async_event={async_event}, lua_event={lua}):")
|
||||
if lua:
|
||||
if event_name not in self.__lua_events:
|
||||
self.__lua_events.update({str(event_name): [event_func]})
|
||||
else:
|
||||
self.__lua_events[event_name].append(event_func)
|
||||
self.log.debug("Register ok")
|
||||
return
|
||||
|
||||
if not callable(event_func):
|
||||
# TODO: i18n
|
||||
self.log.error(f"Cannot add event '{event_name}'. "
|
||||
@ -62,11 +87,13 @@ class EventsSystem:
|
||||
self.__async_events.update({str(event_name): [event_func]})
|
||||
else:
|
||||
self.__async_events[event_name].append(event_func)
|
||||
self.log.debug("Register ok")
|
||||
else:
|
||||
if event_name not in self.__events:
|
||||
self.__events.update({str(event_name): [event_func]})
|
||||
else:
|
||||
self.__events[event_name].append(event_func)
|
||||
self.log.debug("Register ok")
|
||||
|
||||
async def call_async_event(self, event_name, *args, **kwargs):
|
||||
self.log.debug(f"Calling async event: '{event_name}'")
|
||||
@ -106,3 +133,24 @@ class EventsSystem:
|
||||
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_async_event()?. Just skipping it...")
|
||||
|
||||
return funcs_data
|
||||
|
||||
def call_lua_event(self, event_name, *args):
|
||||
self.log.debug(f"Calling lua event: '{event_name}'")
|
||||
funcs_data = []
|
||||
if event_name in self.__lua_events.keys():
|
||||
for func in self.__lua_events[event_name]:
|
||||
try:
|
||||
funcs_data.append(func(*args))
|
||||
except Exception as e:
|
||||
# TODO: i18n
|
||||
self.log.error(f'Error while calling "{event_name}"; In function: "{func.__name__}"')
|
||||
self.log.exception(e)
|
||||
else:
|
||||
# TODO: i18n
|
||||
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_lua_event() or MP.Trigger<>Event()?. "
|
||||
f"Just skipping it...")
|
||||
|
||||
return funcs_data
|
||||
|
||||
|
||||
|
||||
|
@ -3,9 +3,11 @@ from typing import Any
|
||||
|
||||
class EventsSystem:
|
||||
@staticmethod
|
||||
def register_event(event_name, event_func): ...
|
||||
def register_event(event_name, event_func, async_event: bool = False, lua: bool | object = None): ...
|
||||
@staticmethod
|
||||
async def call_async_event(event_name, *args, **kwargs) -> list[Any]: ...
|
||||
@staticmethod
|
||||
def call_event(event_name, *data, **kwargs) -> list[Any]: ...
|
||||
@staticmethod
|
||||
def call_lua_event(event_name, *data) -> list[Any]: ...
|
||||
class ev(EventsSystem): ...
|
||||
|
@ -1,4 +1,26 @@
|
||||
function MP:GetServerVersion()
|
||||
ver = MP:_GetServerVersion()
|
||||
return ver[0], ver[1], ver[2]
|
||||
package.path = package.path..";modules/PluginsLoader/lua_libs/?.lua"
|
||||
|
||||
MP.Timer = {}
|
||||
function MP.CreateTimer()
|
||||
local newObj = {
|
||||
startTime = os.clock()
|
||||
}
|
||||
setmetatable(newObj, { __index = MP.Timer })
|
||||
return newObj
|
||||
end
|
||||
function MP.Timer:GetCurrent()
|
||||
return os.clock() - self.startTime
|
||||
end
|
||||
function MP.Timer:Start()
|
||||
self.startTime = os.clock()
|
||||
end
|
||||
|
||||
function MP.Sleep(time_ms)
|
||||
local start = getTickCount()
|
||||
while getTickCount() - start < time_ms do end
|
||||
end
|
||||
|
||||
MP.CallStrategy = {
|
||||
BestEffort = 0,
|
||||
Precise = 1
|
||||
}
|
||||
|
@ -1,10 +1,7 @@
|
||||
import asyncio
|
||||
import os
|
||||
import platform
|
||||
from typing import Tuple, List, Any
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
import lupa.lua53
|
||||
from lupa.lua53 import LuaRuntime
|
||||
|
||||
from core import get_logger
|
||||
@ -13,24 +10,147 @@ from core import get_logger
|
||||
# noinspection PyPep8Naming
|
||||
class MP:
|
||||
|
||||
def __init__(self, name):
|
||||
# In ./in_lua.lua
|
||||
# MP.CreateTimer
|
||||
# MP.Sleep
|
||||
|
||||
def __init__(self, name: str, lua: LuaRuntime):
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.tasks = []
|
||||
self.name = name
|
||||
self.log = get_logger(f"LuaPlugin | {name}")
|
||||
self._lua = lua
|
||||
|
||||
def _print_log(self, *args):
|
||||
s = ""
|
||||
for i in args:
|
||||
s += f" {i}"
|
||||
def _print(self, *args):
|
||||
s = " ".join(map(str, args))
|
||||
self.log.info(s)
|
||||
|
||||
def CreateTimer(self): ...
|
||||
|
||||
def GetOSName(self) -> str:
|
||||
return platform.system()
|
||||
self.log.debug("request MP.GetOSName()")
|
||||
pl = platform.system()
|
||||
if pl in ["Linux", "Windows"]:
|
||||
return pl
|
||||
return "Other"
|
||||
|
||||
def _GetServerVersion(self) -> tuple[int, int, int]:
|
||||
major, minor, patch = ev.call_event("_get_BeamMP_version")[0]
|
||||
return major, minor, patch
|
||||
def GetServerVersion(self) -> tuple[int, int, int]:
|
||||
self.log.debug("request MP.GetServerVersion()")
|
||||
return ev.call_event("_get_BeamMP_version")[0]
|
||||
|
||||
def RegisterEvent(self, event_name: str, function_name: str) -> None:
|
||||
self.log.debug("request MP.RegisterEvent()")
|
||||
ev.register_event(event_name, self._lua.globals()[function_name], lua=True)
|
||||
|
||||
def TriggerLocalEvent(self, event_name, *args):
|
||||
self.log.debug("request TriggerLocalEvent()")
|
||||
# TODO: TriggerLocalEvent
|
||||
return self._lua.table()
|
||||
|
||||
def TriggerGlobalEvent(self, event_name, *args):
|
||||
self.log.debug("request TriggerGlobalEvent()")
|
||||
# TODO: TriggerGlobalEvent
|
||||
return self._lua.table(IsDone=lambda: True, GetResults=lambda: "somedata")
|
||||
|
||||
def SendChatMessage(self, player_id, message):
|
||||
self.log.debug("request SendChatMessage()")
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
to_all = False
|
||||
if player_id < 0:
|
||||
to_all = True
|
||||
client = client[0]
|
||||
if client and message:
|
||||
t = self.loop.create_task(client.send_message(f"Server: {message}", to_all=to_all))
|
||||
self.tasks.append(t)
|
||||
|
||||
def TriggerClientEvent(self, player_id, event_name, data):
|
||||
# TODO: TriggerClientEvent
|
||||
self.log.debug("request TriggerClientEvent()")
|
||||
|
||||
def TriggerClientEventJson(self, player_id, event_name, data):
|
||||
# TODO: TriggerClientEventJson
|
||||
self.log.debug("request TriggerClientEventJson()")
|
||||
|
||||
def GetPlayerCount(self):
|
||||
self.log.debug("request GetPlayerCount()")
|
||||
return len(ev.call_event("_get_player", cid=-1)[0])
|
||||
|
||||
def GetPositionRaw(self, player_id, car_id):
|
||||
self.log.debug("request GetPositionRaw()")
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
car = client.cars[car_id]
|
||||
if car:
|
||||
return self._lua.table_from(car['pos'])
|
||||
return self._lua.table(), "Vehicle not found"
|
||||
return self._lua.table(), "Client expired"
|
||||
|
||||
def IsPlayerConnected(self, player_id):
|
||||
self.log.debug("request IsPlayerConnected()")
|
||||
return bool(ev.call_event("_get_player", cid=player_id)[0])
|
||||
|
||||
def GetPlayerName(self, player_id):
|
||||
self.log.debug("request GetPlayerName()")
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
return client.nick
|
||||
return
|
||||
|
||||
def RemoveVehicle(self, player_id, vehicle_id):
|
||||
self.log.debug("request GetPlayerName()")
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
t = self.loop.create_task(client._delete_car(car_id=vehicle_id))
|
||||
self.tasks.append(t)
|
||||
|
||||
def GetPlayerVehicles(self, player_id):
|
||||
self.log.debug("request GetPlayerVehicles()")
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
return self._lua.table_from(
|
||||
{i: f'{v["json"]}' for i, d in enumerate([i for i in client.cars if i is not None]) for k, v in
|
||||
d.items() if k == "json"})
|
||||
|
||||
def GetPlayers(self):
|
||||
self.log.debug("request GetPlayers()")
|
||||
clients = ev.call_event("_get_players", cid=-1)
|
||||
return self._lua.table_from({i: n for i, n in enumerate(clients)})
|
||||
|
||||
def IsPlayerGuest(self, player_id) -> bool:
|
||||
self.log.debug("request IsPlayerGuest()")
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
return client.guest
|
||||
return False
|
||||
|
||||
def DropPlayer(self, player_id, reason="Kicked"):
|
||||
self.log.debug("request DropPlayer()")
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
t = self.loop.create_task(client.kick(reason))
|
||||
self.tasks.append(t)
|
||||
|
||||
def GetStateMemoryUsage(self):
|
||||
self.log.debug("request GetStateMemoryUsage()")
|
||||
return self._lua.get_memory_used()
|
||||
|
||||
def GetLuaMemoryUsage(self):
|
||||
self.log.debug("request GetStateMemoryUsage()")
|
||||
lua_plugins = ev.call_event("_lua_plugins_get")[0]
|
||||
return sum(pl['lua'].get_memory_used() for pls in lua_plugins.values() for pl in pls.values())
|
||||
|
||||
def GetPlayerIdentifiers(self, player_id):
|
||||
self.log.debug("request GetStateMemoryUsage()")
|
||||
client = ev.call_event("_get_player", cid=player_id)[0]
|
||||
if client:
|
||||
return self._lua.table_from(client.identifiers)
|
||||
return self._lua.table()
|
||||
|
||||
def Set(self, *args):
|
||||
self.log.debug("request Set")
|
||||
self.log.warning("KuiToi cannot support this: MP.Set()")
|
||||
|
||||
def Settings(self, *args):
|
||||
self.log.debug("request Set")
|
||||
self.log.warning("KuiToi cannot support this: MP.Settings()")
|
||||
|
||||
|
||||
class LuaPluginsLoader:
|
||||
@ -43,6 +163,7 @@ class LuaPluginsLoader:
|
||||
self.lua_dirs = []
|
||||
self.log = get_logger("LuaPluginsLoader")
|
||||
self.loaded_str = "Lua plugins: "
|
||||
ev.register_event("_lua_plugins_get", lambda x: self.lua_plugins)
|
||||
ev.register_event("_lua_plugins_start", self.start)
|
||||
ev.register_event("_lua_plugins_unload", self.unload)
|
||||
console.add_command("lua_plugins", lambda x: self.loaded_str[:-2])
|
||||
@ -61,21 +182,27 @@ class LuaPluginsLoader:
|
||||
|
||||
for path, obj in self.lua_dirs:
|
||||
# noinspection PyArgumentList
|
||||
lua = LuaRuntime(encoding=config.enc, source_encoding=config.enc)
|
||||
mp = MP(obj)
|
||||
lua.globals().MP = mp
|
||||
lua = LuaRuntime(encoding=config.enc, source_encoding=config.enc, unpack_returned_tuples=True)
|
||||
lua.globals().printRaw = lua.globals().print
|
||||
lua.globals().print = mp._print_log
|
||||
lua.globals().exit = lambda x: self.log.info(f"{obj}: You can't disable server..")
|
||||
mp = MP(obj, lua)
|
||||
lua.globals().MP = mp
|
||||
lua.globals().print = mp._print
|
||||
code = f'package.path = package.path.."' \
|
||||
f';{self.plugins_dir}/{obj}/?.lua' \
|
||||
f';{self.plugins_dir}/{obj}/lua/?.lua' \
|
||||
f';modules/PluginsLoader/lua_libs/?.lua"\n'
|
||||
f';{self.plugins_dir}/{obj}/lua/?.lua"\n'
|
||||
with open("modules/PluginsLoader/add_in.lua", "r") as f:
|
||||
code += f.read()
|
||||
with open(os.path.join(path, "main.lua"), 'r', encoding=config.enc) as f:
|
||||
code += f.read()
|
||||
lua.execute(code)
|
||||
try:
|
||||
lua.execute(code)
|
||||
self.loaded_str += f"{obj}:ok, "
|
||||
self.lua_plugins.update({obj: {"mp": mp, "lua": lua}})
|
||||
except Exception as e:
|
||||
self.log.error(f"Cannot load lua plugin from `{obj}/main.lua`")
|
||||
self.log.exception(e)
|
||||
self.loaded_str += f"{obj}:no, "
|
||||
|
||||
async def start(self, _):
|
||||
...
|
||||
|
Loading…
x
Reference in New Issue
Block a user