add support for setting points, tokens, and broadcasting control msgs

Control messages are messages that are broadcast to all clients and have
no clearly defined content.  The idea is that this can be used to
control a monitor without having to keep adding support for specific
commands on the protocol layer.
This also changes some of the existing messages and adds another
ridiculous convenience layer to our HTML/JS templating: data-eval.
We should probably just bite the bullet and use some reactive framework.
This commit is contained in:
ducklet 2021-02-01 23:07:49 +01:00
parent f406627042
commit b00f8a357c
4 changed files with 242 additions and 65 deletions

View file

@ -31,7 +31,8 @@ class Client:
id: UserId = field(default_factory=token)
is_reclaimed: bool = False
name: str = ""
points: int = 0
play_points: int = 0 # The points the player has accumulated.
play_tokens: int = 0 # The action tokens the player has left.
secret: Token = field(default_factory=token)
session: Optional["Session"] = None
@ -83,6 +84,8 @@ class Client:
"name": self.name or "<noname>",
"id": self.id,
"active": self.is_active,
"points": self.play_points,
"tokens": self.play_tokens,
}
def _reclaim(self, other: "Client"):
@ -104,7 +107,8 @@ class Client:
# Load all relevant info from other.
self.id = other.id
self.name = other.name
self.points = other.points
self.play_points = other.play_points
self.play_tokens = other.play_tokens
self.secret = other.secret
# Invalidate other.
@ -123,6 +127,11 @@ class Session:
@classmethod
def get(cls, client: Client) -> "Session":
"""Return the session for the client.
A new session will automatically be created if the given client
is not yet associated with any session.
"""
is_new = client.path not in cls.sessions
if is_new:
if len(cls.sessions) >= config.max_sessions:
@ -172,12 +181,12 @@ class LoginError(RuntimeError):
def msg(type_: str, **args):
return dumps({"type": type_, **args})
return dumps({"type": type_, "value": args})
async def send_time(target: Client):
"""Send the current server time to the target."""
await target.send("time", value=perf_counter_ns())
await target.send("time", time=perf_counter_ns())
async def send_buzz(target: Client, client: Client, time: int):
@ -187,12 +196,17 @@ async def send_buzz(target: Client, client: Client, time: int):
async def send_clients(target: Client):
"""Send info about all connected clients of a session to the target."""
await target.send("clients", value=[c.info for c in target.session_clients()])
await target.send("clients", clients=[c.info for c in target.session_clients()])
async def send_client(target: Client, client: Client):
"""Send info about the client to the target."""
await target.send("client", **client.info)
await target.send("client", client=client.info)
async def send_control(target: Client, payload: Any):
"""Send a control message to the target."""
await target.send("control", payload=payload)
async def wait(coros, **kwds):
@ -223,6 +237,11 @@ async def broadcast_buzz(client: Client, time: int):
await wait(send_buzz(c, client, time) for c in client.session_clients())
async def broadcast_control(session: Session, payload: Any):
"""Send a control message to all clients of a session."""
await wait(send_control(c, payload) for c in session.clients.values())
async def send_credentials(target: Client):
"""Send their user credentials to a client."""
await target.send("id", id=target.id, key=target.secret, path=target.path)
@ -253,9 +272,11 @@ def printable(s: str) -> str:
async def handle_messages(client: Client):
assert client.session
async for message in client.messages:
log.debug("[%s] got a message: %a", client, message)
mdata = loads(message)
# XXX we should probably define & check the types (well-formed) here??
if mdata["type"] == "buzz":
time = mdata["value"]
log.info("[%s] buzz: %a", client, time)
@ -266,28 +287,44 @@ async def handle_messages(client: Client):
log.info("[%s] new name: %a", client, name)
client.name = name
await broadcast_client(client)
elif mdata["type"] == "login":
assert client.session
elif mdata["type"] == "control":
if not client.is_admin:
log.info("[%s] not authorized to send control messages.")
control_data = mdata["value"]
await broadcast_control(client.session, control_data)
elif mdata["type"] in ("points", "tokens"):
if not client.is_admin:
log.info("[%s] not authorized to set points or tokens.")
target_id = UserId(mdata["value"]["id"])
existent = client.session.clients.get(target_id)
if existent is None:
points = int(mdata["value"][mdata["type"]])
target = client.session.clients.get(target_id)
if target is None:
log.info("[%s] no such user: %a", client, target_id)
return
if mdata["type"] == "points":
target.play_points = points
elif mdata["type"] == "tokens":
target.play_tokens = points
await broadcast_client(client)
elif mdata["type"] == "login":
target_id = UserId(mdata["value"]["id"])
target = client.session.clients.get(target_id)
if target is None:
log.info(
"[%s] tried to log in as non-existent user: %a", client, target_id
)
return
if existent.is_active:
if target.is_active:
log.info("[%s] cannot log in as active user: %a", client, target_id)
return
if existent.is_reclaimed:
log.info("[%s] client already reclaimed: %a", client, target_id)
if target.is_reclaimed:
log.info("[%s] target already reclaimed: %a", client, target_id)
return
if not compare_digest(mdata["value"]["key"], existent.secret):
log.info(
"[%s] failed to log in as existing user: %a", client, target_id
)
if not compare_digest(mdata["value"]["key"], target.secret):
log.info("[%s] failed to log in as target user: %a", client, target_id)
return
log.info("[%s] logging in as existent user: %s", client, existent)
client.session.reclaim(client, inactive=existent)
log.info("[%s] reclaiming target user: %s", client, target)
client.session.reclaim(client, inactive=target)
await send_hello(client)
await broadcast_clients(client.session)
else: