decorators for server

This commit is contained in:
Christoph Stahl 2024-10-14 15:32:06 +02:00
parent 8900251b1e
commit cd3e6d6d7c

View file

@ -23,7 +23,7 @@ from json.decoder import JSONDecodeError
from argparse import Namespace from argparse import Namespace
from dataclasses import dataclass from dataclasses import dataclass
from dataclasses import field from dataclasses import field
from typing import Any from typing import Any, Callable, overload
from typing import AsyncGenerator from typing import AsyncGenerator
from typing import Optional from typing import Optional
@ -48,6 +48,70 @@ DEFAULT_CONFIG = {
} }
def with_state(handler: Callable[..., Any]) -> Callable[..., Any]:
"""
Decorator that forwards the state of a room to a handler.
:param forward_room: Either the handler to decorate or a boolean
defining if the room should be forwarded.
:type forward_room: bool | Callable[..., Any]
:return: The decorated handler or a function that decorates a handler
:rtype: Callable[..., Any] | Callable[[Callable[..., Any]], Callable[..., Any]]
"""
async def wrapper(self: Server, sid: str, *args: Any, **kwargs: Any) -> Any:
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
return await handler(self, state, sid, *args, **kwargs)
return wrapper
def admin(handler: Callable[..., Any]) -> Callable[..., Any]:
"""
Decorator, that requires the client to be an admin.
If the client is not an admin, the handler is not called.
:param handler: The handler to decorate
:type handler: Callable[..., Any]
:return: The decorated handler
:rtype: Callable[..., Any]
"""
async def wrapper(self: Server, sid: str, *args: Any, **kwargs: Any) -> Any:
async with self.sio.session(sid) as session:
if not session["admin"]:
await self.sio.emit("err", {"type": "NO_ADMIN"}, sid)
return await handler(self, sid, *args, **kwargs)
return wrapper
def playback(handler: Callable[..., Any]) -> Callable[..., Any]:
"""
Decorator, that requires the client to be a playback client.
If the client is not a playback client, the handler is not called.
:param handler: The handler to decorate
:type handler: Callable[..., Any]
:return: The decorated handler
:rtype: Callable[..., Any]
"""
async def wrapper(self: Server, sid: str, *args: Any, **kwargs: Any) -> Any:
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
if sid != state.sid:
return
return await handler(self, sid, *args, **kwargs)
return wrapper
@dataclass @dataclass
class Client: class Client:
"""This stores the configuration of a specific playback client. """This stores the configuration of a specific playback client.
@ -165,6 +229,11 @@ class Server:
return web.FileResponse(os.path.join(self.app["root_folder"], "favicon.ico")) return web.FileResponse(os.path.join(self.app["root_folder"], "favicon.ico"))
return web.FileResponse(os.path.join(self.app["root_folder"], "index.html")) return web.FileResponse(os.path.join(self.app["root_folder"], "index.html"))
async def broadcast_state(self, state: State) -> None:
async with self.sio.session(state.sid) as session:
room = session["room"]
await self.send_state(state, room)
async def send_state(self, state: State, sid: str) -> None: async def send_state(self, state: State, sid: str) -> None:
""" """
Send the current state (queue and recent-list) to sid. Send the current state (queue and recent-list) to sid.
@ -195,7 +264,8 @@ class Server:
room=sid, room=sid,
) )
async def handle_get_state(self, sid: str) -> None: @with_state
async def handle_get_state(self, state: State, sid: str) -> None:
""" """
Handle the "get-state" message. Handle the "get-state" message.
@ -207,13 +277,12 @@ class Server:
:type sid: str :type sid: str
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
await self.send_state(state, sid) await self.send_state(state, sid)
async def handle_waiting_room_append(self, sid: str, data: dict[str, Any]) -> None: @with_state
async def handle_waiting_room_append(
self, state: State, sid: str, data: dict[str, Any]
) -> None:
""" """
Append a song to the waiting room. Append a song to the waiting room.
@ -228,12 +297,7 @@ class Server:
:type data: dict[str, Any] :type data: dict[str, Any]
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
source_obj = state.client.sources[data["source"]] source_obj = state.client.sources[data["source"]]
entry = await source_obj.get_entry(data["performer"], data["ident"]) entry = await source_obj.get_entry(data["performer"], data["ident"])
if entry is None: if entry is None:
@ -248,21 +312,21 @@ class Server:
(data["uid"] is not None and len(list(state.queue.find_by_uid(data["uid"]))) == 0) (data["uid"] is not None and len(list(state.queue.find_by_uid(data["uid"]))) == 0)
or (data["uid"] is None and state.queue.find_by_name(data["performer"]) is None) or (data["uid"] is None and state.queue.find_by_name(data["performer"]) is None)
): ):
await self.append_to_queue(room, entry, sid) await self.append_to_queue(state, entry, sid)
return return
entry.uid = data["uid"] entry.uid = data["uid"]
state.waiting_room.append(entry) state.waiting_room.append(entry)
await self.send_state(state, room) await self.broadcast_state(state)
await self.sio.emit( await self.sio.emit(
"get-meta-info", "get-meta-info",
entry, entry,
room=self.clients[room].sid, room=state.sid,
) )
async def append_to_queue( async def append_to_queue(
self, room: str, entry: Entry, report_to: Optional[str] = None self, state: State, entry: Entry, report_to: Optional[str] = None
) -> None: ) -> None:
""" """
Append a song to the queue for a given session. Append a song to the queue for a given session.
@ -278,8 +342,6 @@ class Server:
:type report_to: Optional[str] :type report_to: Optional[str]
:rtype: None :rtype: None
""" """
state = self.clients[room]
first_song = state.queue.try_peek() first_song = state.queue.try_peek()
if first_song is None or first_song.started_at is None: if first_song is None or first_song.started_at is None:
start_time = datetime.datetime.now().timestamp() start_time = datetime.datetime.now().timestamp()
@ -305,15 +367,17 @@ class Server:
return return
state.queue.append(entry) state.queue.append(entry)
await self.send_state(state, room) await self.broadcast_state(state)
await self.sio.emit( await self.sio.emit(
"get-meta-info", "get-meta-info",
entry, entry,
room=self.clients[room].sid, room=state.sid,
) )
async def handle_show_config(self, sid: str) -> None: @admin
@with_state
async def handle_show_config(self, state: State, sid: str) -> None:
""" """
Sends public config to webclient. Sends public config to webclient.
@ -323,22 +387,15 @@ class Server:
:type sid: str :type sid: str
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
is_admin = session["admin"]
state = self.clients[room]
if is_admin:
await self.sio.emit( await self.sio.emit(
"config", "config",
state.client.config, state.client.config,
sid, sid,
) )
else:
await self.sio.emit("err", {"type": "NO_ADMIN"}, sid)
async def handle_update_config(self, sid: str, data: dict[str, Any]) -> None: @admin
@with_state
async def handle_update_config(self, state: State, sid: str, data: dict[str, Any]) -> None:
""" """
Forwards an updated config from an authorized webclient to the playback client. Forwards an updated config from an authorized webclient to the playback client.
@ -350,12 +407,6 @@ class Server:
:type data: dict[str, Any] :type data: dict[str, Any]
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
is_admin = session["admin"]
state = self.clients[room]
if is_admin:
try: try:
config = jsonencoder.loads(data["config"]) config = jsonencoder.loads(data["config"])
await self.sio.emit( await self.sio.emit(
@ -364,14 +415,12 @@ class Server:
state.sid, state.sid,
) )
state.client.config = DEFAULT_CONFIG | config state.client.config = DEFAULT_CONFIG | config
await self.sio.emit("update_config", config, room) # await self.sio.emit("update_config", config, room)
except JSONDecodeError: except JSONDecodeError:
await self.sio.emit("err", {"type": "JSON_MALFORMED"}) await self.sio.emit("err", {"type": "JSON_MALFORMED"}, room=sid)
else: @with_state
await self.sio.emit("err", {"type": "NO_ADMIN"}, sid) async def handle_append(self, state: State, sid: str, data: dict[str, Any]) -> None:
async def handle_append(self, sid: str, data: dict[str, Any]) -> None:
""" """
Handle the "append" message. Handle the "append" message.
@ -406,10 +455,6 @@ class Server:
:type data: dict[str, Any] :type data: dict[str, Any]
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
if len(data["performer"]) > 50: if len(data["performer"]) > 50:
await self.sio.emit("err", {"type": "NAME_LENGTH", "name": data["performer"]}, room=sid) await self.sio.emit("err", {"type": "NAME_LENGTH", "name": data["performer"]}, room=sid)
return return
@ -456,9 +501,10 @@ class Server:
entry.uid = data["uid"] if "uid" in data else None entry.uid = data["uid"] if "uid" in data else None
await self.append_to_queue(room, entry, sid) await self.append_to_queue(state, entry, sid)
async def handle_append_anyway(self, sid: str, data: dict[str, Any]) -> None: @with_state
async def handle_append_anyway(self, state: State, sid: str, data: dict[str, Any]) -> None:
""" """
Appends a song to the queue, even if the performer is already in queue. Appends a song to the queue, even if the performer is already in queue.
@ -467,10 +513,6 @@ class Server:
Only if the waiting_room_policy is not configured as forced. Only if the waiting_room_policy is not configured as forced.
""" """
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
if len(data["performer"]) > 50: if len(data["performer"]) > 50:
await self.sio.emit("err", {"type": "NAME_LENGTH", "name": data["performer"]}, room=sid) await self.sio.emit("err", {"type": "NAME_LENGTH", "name": data["performer"]}, room=sid)
return return
@ -501,9 +543,11 @@ class Server:
entry.uid = data["uid"] if "uid" in data else None entry.uid = data["uid"] if "uid" in data else None
await self.append_to_queue(room, entry, sid) await self.append_to_queue(state, entry, sid)
async def handle_meta_info(self, sid: str, data: dict[str, Any]) -> None: @playback
@with_state
async def handle_meta_info(self, state: State, sid: str, data: dict[str, Any]) -> None:
""" """
Handle the "meta-info" message. Handle the "meta-info" message.
@ -520,10 +564,6 @@ class Server:
:type data: dict[str, Any] :type data: dict[str, Any]
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
state.queue.update( state.queue.update(
data["uuid"], data["uuid"],
lambda item: item.update(**data["meta"]), lambda item: item.update(**data["meta"]),
@ -533,9 +573,11 @@ class Server:
if entry.uuid == data["uuid"] or str(entry.uuid) == data["uuid"]: if entry.uuid == data["uuid"] or str(entry.uuid) == data["uuid"]:
entry.update(**data["meta"]) entry.update(**data["meta"])
await self.send_state(state, room) await self.broadcast_state(state)
async def handle_get_first(self, sid: str) -> None: @playback
@with_state
async def handle_get_first(self, state: State, sid: str) -> None:
""" """
Handle the "get-first" message. Handle the "get-first" message.
@ -553,16 +595,16 @@ class Server:
:type sid: str :type sid: str
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
current = await state.queue.peek() current = await state.queue.peek()
current.started_at = datetime.datetime.now().timestamp() current.started_at = datetime.datetime.now().timestamp()
await self.sio.emit("play", current, room=sid) await self.sio.emit("play", current, room=sid)
async def handle_waiting_room_to_queue(self, sid: str, data: dict[str, Any]) -> None: @admin
@with_state
async def handle_waiting_room_to_queue(
self, state: State, sid: str, data: dict[str, Any]
) -> None:
""" """
Handle the "waiting-room-to-queue" message. Handle the "waiting-room-to-queue" message.
@ -573,21 +615,15 @@ class Server:
:type sid: str :type sid: str
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
is_admin = session["admin"]
state = self.clients[room]
if is_admin:
entry = next( entry = next(
(wr_entry for wr_entry in state.waiting_room if str(wr_entry.uuid) == data["uuid"]), (wr_entry for wr_entry in state.waiting_room if str(wr_entry.uuid) == data["uuid"]),
None, None,
) )
if entry is not None: if entry is not None:
state.waiting_room.remove(entry) state.waiting_room.remove(entry)
await self.append_to_queue(room, entry, sid) await self.append_to_queue(state, entry, sid)
async def add_songs_from_waiting_room(self, room: str) -> None: async def add_songs_from_waiting_room(self, state: State) -> None:
""" """
Add all songs from the waiting room, that should be added to the queue. Add all songs from the waiting room, that should be added to the queue.
@ -599,18 +635,16 @@ class Server:
:type room: str :type room: str
:rtype: None :rtype: None
""" """
state = self.clients[room]
wrs_to_remove = [] wrs_to_remove = []
for wr_entry in state.waiting_room: for wr_entry in state.waiting_room:
if state.queue.find_by_name(wr_entry.performer) is None: if state.queue.find_by_name(wr_entry.performer) is None:
await self.append_to_queue(room, wr_entry) await self.append_to_queue(state, wr_entry)
wrs_to_remove.append(wr_entry) wrs_to_remove.append(wr_entry)
for wr_entry in wrs_to_remove: for wr_entry in wrs_to_remove:
state.waiting_room.remove(wr_entry) state.waiting_room.remove(wr_entry)
async def discard_first(self, room: str) -> Entry: async def discard_first(self, state: State) -> Entry:
""" """
Gets the first element of the queue, handling resulting triggers. Gets the first element of the queue, handling resulting triggers.
@ -622,18 +656,19 @@ class Server:
:type room: str :type room: str
:rtype: Entry :rtype: Entry
""" """
state = self.clients[room]
old_entry = await state.queue.popleft() old_entry = await state.queue.popleft()
await self.add_songs_from_waiting_room(room) await self.add_songs_from_waiting_room(state)
state.recent.append(old_entry) state.recent.append(old_entry)
state.last_seen = datetime.datetime.now() state.last_seen = datetime.datetime.now()
return old_entry return old_entry
async def handle_pop_then_get_next(self, sid: str) -> None: @playback
@with_state
async def handle_pop_then_get_next(self, state: State, sid: str) -> None:
""" """
Handle the "pop-then-get-next" message. Handle the "pop-then-get-next" message.
@ -651,19 +686,12 @@ class Server:
:type sid: str :type sid: str
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session: await self.discard_first(state)
room = session["room"] await self.broadcast_state(state)
state = self.clients[room]
if sid != state.sid:
return
await self.discard_first(room)
await self.send_state(state, room)
current = await state.queue.peek() current = await state.queue.peek()
current.started_at = datetime.datetime.now().timestamp() current.started_at = datetime.datetime.now().timestamp()
await self.send_state(state, room) await self.broadcast_state(state)
await self.sio.emit("play", current, room=sid) await self.sio.emit("play", current, room=sid)
@ -795,7 +823,9 @@ class Server:
await self.sio.emit("client-registered", {"success": True, "room": room}, room=sid) await self.sio.emit("client-registered", {"success": True, "room": room}, room=sid)
await self.send_state(self.clients[room], sid) await self.send_state(self.clients[room], sid)
async def handle_sources(self, sid: str, data: dict[str, Any]) -> None: @playback
@with_state
async def handle_sources(self, state: State, sid: str, data: dict[str, Any]) -> None:
""" """
Handle the "sources" message. Handle the "sources" message.
@ -816,13 +846,6 @@ class Server:
:type data: dict[str, Any] :type data: dict[str, Any]
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
if sid != state.sid:
return
unused_sources = state.client.sources.keys() - data["sources"] unused_sources = state.client.sources.keys() - data["sources"]
new_sources = data["sources"] - state.client.sources.keys() new_sources = data["sources"] - state.client.sources.keys()
@ -834,7 +857,9 @@ class Server:
for name in new_sources: for name in new_sources:
await self.sio.emit("request-config", {"source": name}, room=sid) await self.sio.emit("request-config", {"source": name}, room=sid)
async def handle_config_chunk(self, sid: str, data: dict[str, Any]) -> None: @playback
@with_state
async def handle_config_chunk(self, state: State, sid: str, data: dict[str, Any]) -> None:
""" """
Handle the "config-chunk" message. Handle the "config-chunk" message.
@ -851,19 +876,14 @@ class Server:
depends on the source. depends on the source.
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
if sid != state.sid:
return
if data["source"] not in state.client.sources: if data["source"] not in state.client.sources:
state.client.sources[data["source"]] = available_sources[data["source"]](data["config"]) state.client.sources[data["source"]] = available_sources[data["source"]](data["config"])
else: else:
state.client.sources[data["source"]].add_to_config(data["config"], data["number"]) state.client.sources[data["source"]].add_to_config(data["config"], data["number"])
async def handle_config(self, sid: str, data: dict[str, Any]) -> None: @playback
@with_state
async def handle_config(self, state: State, sid: str, data: dict[str, Any]) -> None:
""" """
Handle the "config" message. Handle the "config" message.
@ -879,13 +899,6 @@ class Server:
:type data: dict[str, Any] :type data: dict[str, Any]
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
if sid != state.sid:
return
state.client.sources[data["source"]] = available_sources[data["source"]](data["config"]) state.client.sources[data["source"]] = available_sources[data["source"]](data["config"])
async def handle_register_web(self, sid: str, data: dict[str, Any]) -> bool: async def handle_register_web(self, sid: str, data: dict[str, Any]) -> bool:
@ -911,7 +924,8 @@ class Server:
return True return True
return False return False
async def handle_register_admin(self, sid: str, data: dict[str, Any]) -> bool: @with_state
async def handle_register_admin(self, state: State, sid: str, data: dict[str, Any]) -> bool:
""" """
Handle a "register-admin" message. Handle a "register-admin" message.
@ -925,16 +939,14 @@ class Server:
:returns: True, if the secret is correct, False otherwise :returns: True, if the secret is correct, False otherwise
:rtype: bool :rtype: bool
""" """
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
is_admin: bool = data["secret"] == state.client.config["secret"] is_admin: bool = data["secret"] == state.client.config["secret"]
async with self.sio.session(sid) as session: async with self.sio.session(sid) as session:
session["admin"] = is_admin session["admin"] = is_admin
return is_admin return is_admin
async def handle_skip_current(self, sid: str) -> None: @admin
@with_state
async def handle_skip_current(self, state: State, sid: str) -> None:
""" """
Handle a "skip-current" message. Handle a "skip-current" message.
@ -946,17 +958,13 @@ class Server:
:type sid: str :type sid: str
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session: old_entry = await self.discard_first(state)
room = session["room"] await self.sio.emit("skip-current", old_entry, room=state.sid)
is_admin = session["admin"] await self.broadcast_state(state)
state = self.clients[room]
if is_admin: @admin
old_entry = await self.discard_first(room) @with_state
await self.sio.emit("skip-current", old_entry, room=self.clients[room].sid) async def handle_move_to(self, state: State, sid: str, data: dict[str, Any]) -> None:
await self.send_state(state, room)
async def handle_move_to(self, sid: str, data: dict[str, Any]) -> None:
""" """
Handle the "move-to" message. Handle the "move-to" message.
@ -969,18 +977,12 @@ class Server:
:type data: dict[str, Any] :type data: dict[str, Any]
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
is_admin = session["admin"]
state = self.clients[room]
if is_admin:
await state.queue.move_to(data["uuid"], data["target"]) await state.queue.move_to(data["uuid"], data["target"])
await self.send_state(state, room) await self.broadcast_state(state)
async def handle_move_up(self, sid: str, data: dict[str, Any]) -> None: @admin
@with_state
async def handle_move_up(self, state: State, sid: str, data: dict[str, Any]) -> None:
""" """
Handle the "move-up" message. Handle the "move-up" message.
@ -993,15 +995,12 @@ class Server:
:type data: dict[str, Any] :type data: dict[str, Any]
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
is_admin = session["admin"]
state = self.clients[room]
if is_admin:
await state.queue.move_up(data["uuid"]) await state.queue.move_up(data["uuid"])
await self.send_state(state, room) await self.broadcast_state(state)
async def handle_skip(self, sid: str, data: dict[str, Any]) -> None: @admin
@with_state
async def handle_skip(self, state: State, sid: str, data: dict[str, Any]) -> None:
""" """
Handle the "skip" message. Handle the "skip" message.
@ -1014,17 +1013,11 @@ class Server:
:type data: dict[str, Any] :type data: dict[str, Any]
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
is_admin = session["admin"]
state = self.clients[room]
if is_admin:
entry = state.queue.find_by_uuid(data["uuid"]) entry = state.queue.find_by_uuid(data["uuid"])
if entry is not None: if entry is not None:
logger.info("Skipping %s", entry) logger.info("Skipping %s", entry)
await self.add_songs_from_waiting_room(room) await self.add_songs_from_waiting_room(state)
await state.queue.remove(entry) await state.queue.remove(entry)
@ -1040,7 +1033,7 @@ class Server:
state.waiting_room[first_entry_index], state.waiting_room[first_entry_index],
) )
del state.waiting_room[first_entry_index] del state.waiting_room[first_entry_index]
await self.send_state(state, room) await self.broadcast_state(state)
async def handle_disconnect(self, sid: str) -> None: async def handle_disconnect(self, sid: str) -> None:
""" """
@ -1055,10 +1048,12 @@ class Server:
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session: async with self.sio.session(sid) as session:
if "room" in session: room = session.get("room")
await self.sio.leave_room(sid, session["room"]) if room is not None:
await self.sio.leave_room(sid, room)
async def handle_search(self, sid: str, data: dict[str, Any]) -> None: @with_state
async def handle_search(self, state: State, sid: str, data: dict[str, Any]) -> None:
""" """
Handle the "search" message. Handle the "search" message.
@ -1075,10 +1070,6 @@ class Server:
:type data: dict[str, str] :type data: dict[str, str]
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
query = data["query"] query = data["query"]
if ( if (
self.app["type"] != "restricted" self.app["type"] != "restricted"
@ -1097,9 +1088,9 @@ class Server:
] ]
await self.send_search_results(sid, results) await self.send_search_results(sid, results)
else: else:
print("Denied")
await self.sio.emit("search", {"query": query, "sid": sid}, room=state.sid) await self.sio.emit("search", {"query": query, "sid": sid}, room=state.sid)
@playback
async def handle_search_results(self, sid: str, data: dict[str, Any]) -> None: async def handle_search_results(self, sid: str, data: dict[str, Any]) -> None:
""" """
Handle the "search-results" message. Handle the "search-results" message.
@ -1117,13 +1108,6 @@ class Server:
:type data: dict[str, Any] :type data: dict[str, Any]
:rtype: None :rtype: None
""" """
async with self.sio.session(sid) as session:
room = session["room"]
state = self.clients[room]
if sid != state.sid:
return
web_sid = data["sid"] web_sid = data["sid"]
results = [Result.from_dict(result) for result in data["results"]] results = [Result.from_dict(result) for result in data["results"]]