diff --git a/README.md b/README.md index e0d270b..03e970d 100644 --- a/README.md +++ b/README.md @@ -51,31 +51,6 @@ The appliction is implemented using the following different components: - When a user logs in with the `dooris` user now, they are always dropped into the `ssh-cli` binary. This works in cases where a command is specified on the users cli or where it isn't. In any case, the `ssh-cli` binary interprets the given command (via the `SSH_ORIGINAL_COMMAND` environment variable), figures out what to do, and contacts the running API with the configured credentials to do it. -### Communication Diagram - -```text -┌─────────────────┐ ┏━━━━━━━━━━━━━━┓ -│ web browser │ ┃ dooris-api ┃ -├┈┈┈┈┈┈┈with┈┈┈┈┈┈┤ ┣┉┉┉┉┉with┉┉┉┉┉┫ ┌──────────┐ -│ dooris-frontend │ ╌ ╌ ⇆ ╌ ╌ ┃ http server ┃ ╭ ╌ ╌ → ╌ ╌ │ SpaceApi │ -└─────────────────┘ ╭╌ ┃ ┃ ╎ └──────────┘ - ╎ ┣┉┉┉┉┉and┉┉┉┉┉┉┫ ╎ ┌───────────┐ ┌───────────┐ ┌──────────┐ - ╎ ┃ http client ┃ ╯ ╭╌ ⇆ ╌ ╌ │ homematic │ ╌ ⇆ ╌ │ homematic │ ╌ ⇆ ╌ │ physical │ - ╎ ┃ ┃ ╌╌ ╯ ╭ ← ╌ │ ccujack │ │ daemon │ │ lock │ - ⇅ ┣┉┉┉┉┉and┉┉┉┉┉┉┫ ╎ └───────────┘ └───────────┘ └──────────┘ - ╎ ┃ mqtt client ┃╌ ← ╌ ╯ ┌────────────────┐ - ╎ ┃ ┃ ╌ ╌ ╌ → ╌ ╌ │ Club-Assistant │ - ╎ ┗━━━━━━━━━━━━━━┛ └────────────────┘ - ╎ - ╰ ╌ ╌ ╌ ╌ ╌ ⇄ ╌ ╌ ╌ ╌ ╮ -┌────────────┐ ┌──────────────────┐ ╎ -│ SSH client │ ╌ ╌ ╌ ⇆ ╌ ╌ ╌│ dooris "ssh-cli" │ ╯ -└────────────┘ └──────────────────┘ -``` - -- Boxes indicate a discrete daemon or running piece of software. Those sometimes include subcomponents of that daemon for clarity. -- Dotted connections indicate information flow while the arrows indicate the direction of that flow - ## Configuration The final application can be configured either via CLI arguments or via environment variables. @@ -96,15 +71,6 @@ THe following table lists all available configuration parameters: | `--ccujack-user` | `DOORIS_CCUJACK_USER` | Yes | *None* | | `--ccujack-password` | `DOORIS_CCUJACK_PASSWORD` | Yes | *None* | | `--static-api-tokens` | `DOORIS_STATIC_API_TOKENS` | No | *None* | -| `--kc-ssh-attr-group` | `DOORIS_KC_SSH_ATTR_GROUP` | No | `doris-ssh-keys` | -| `--authorized-keys-file` | `DOORIS_AUTHORIZED_KEYS_FILE` | Yes | *None* | -| `--spaceapid-base-uri` | `DOORIS_SPACEAPID_BASE_URI` | No | *None* | -| `--spaceapid-user` | `DOORIS_SPACEAPID_USER` | No | `dooris` | -| `--spaceapid-password` | `DOORIS_SPACEAPID_PASSWORD` | Yes | *None* | -| `--main-lock` | `DOORIS_MAIN_LOCK` | No | *None* | -| `--home-assistant-mqtt` | `DOORIS_HOME_ASSISTANT_MQTT` | No | `club-assistant.ccchh.net:1883` | -| `--home-assistant-mqtt-user` | `DOORIS_HOME_ASSISTANT_MQTT_USER` | No | `dooris` | -| `--home-assistant-mqtt-password` | `DOORIS_HOME_ASSISTANT_MQTT_PASSWORD` | Yes | *None* | ## API Development diff --git a/api/src/dooris_api/__init__.py b/api/src/dooris_api/__init__.py index 2140b6e..e096e98 100644 --- a/api/src/dooris_api/__init__.py +++ b/api/src/dooris_api/__init__.py @@ -123,24 +123,6 @@ def main(): default=os.environ.get("DOORIS_MAIN_LOCK", None), help="Name or ID of the door which will be considered the main door for spaceapi. If none is set, integration to spaceapi is disabled", ) - argp.add_argument( - "--home-assistant-mqtt", - required=False, - default=os.environ.get("DOORIS_HOME_ASSISTANT_MQTT", "club-assistant.ccchh.net:1883"), - help="$HOSTNAME:$PORT of an MQTT server connected to our Home-Assistant", - ) - argp.add_argument( - "--home-assistant-mqtt-user", - required=False, - default=os.environ.get("DOORIS_HOME_ASSISTANT_MQTT_USER", "dooris"), - help="Username to authenticate towards our Home-Assistant MQTT server", - ) - argp.add_argument( - "--home-assistant-mqtt-password", - required="DOORIS_HOME_ASSISTANT_MQTT_PASSWORD" not in os.environ, - default=os.environ.get("DOORIS_HOME_ASSISTANT_MQTT_PASSWORD", None), - help="Password to authenticate towards our Home-Assistant MQTT server", - ) args = argp.parse_args() diff --git a/api/src/dooris_api/app.py b/api/src/dooris_api/app.py index a0d574b..f239fa5 100644 --- a/api/src/dooris_api/app.py +++ b/api/src/dooris_api/app.py @@ -15,7 +15,6 @@ from dooris_api import deps, models, exceptions, app_config from dooris_api.ccujack import CCUJackClient from dooris_api.keycloak import KeycloakClient from dooris_api.spaceapi import SpaceApiClient -from dooris_api.home_assistant import HomeAssistantClient logger = logging.getLogger(__name__) @@ -33,14 +32,6 @@ async def lifespan(app: FastAPI): scope=app_cfg.openid_scope, ) - app.extra["home_assistant"] = HomeAssistantClient( - auth=BasicAuth( - app_cfg.home_assistant_mqtt_user, app_cfg.home_assistant_mqtt_password - ), - mqtt_conn=app_cfg.home_assistant_mqtt, - ) - await app.extra["home_assistant"].start() - app.extra["spaceapi"] = SpaceApiClient( base_uri=app_cfg.spaceapid_base_uri, auth=BasicAuth(app_cfg.spaceapid_user, app_cfg.spaceapid_password), @@ -52,7 +43,6 @@ async def lifespan(app: FastAPI): mqtt_conn=app_cfg.ccujack_mqtt, spaceapi_client=app.extra["spaceapi"], main_lock=app_cfg.main_lock, - home_assistant=app.extra["home_assistant"], ) await app.extra["ccujack"].start() @@ -66,11 +56,8 @@ async def lifespan(app: FastAPI): yield - await asyncio.gather( - app.extra["ccujack"].close_connections(), - app.extra["home_assistant"].close_connections(), - app.extra["keycloak"].stop(), - ) + await app.extra["ccujack"].close_connections() + await app.extra["keycloak"].stop() app = FastAPI( @@ -179,7 +166,7 @@ async def login_callback( return "/" else: logger.debug( - f"could not authenticate user because of OIDC error '{auth_result}'; redirecting to error page with error messages intact" + "could not authenticate user because of OIDC error; redirecting to error page with error messages intact" ) return f"/login-error?{req.query_params}" diff --git a/api/src/dooris_api/ccujack.py b/api/src/dooris_api/ccujack.py index f0dde7a..c3a4d89 100644 --- a/api/src/dooris_api/ccujack.py +++ b/api/src/dooris_api/ccujack.py @@ -6,7 +6,6 @@ from pydantic import BaseModel, Field from dooris_api.mqtt_client import AsyncMqttClient from dooris_api.spaceapi import SpaceApiClient -from dooris_api.home_assistant import HomeAssistantClient logger = logging.getLogger(__name__) @@ -77,18 +76,9 @@ class CCUJackClient: task_find_locks: asyncio.Task data_updated: asyncio.Event spaceapi_client: SpaceApiClient - home_assistant_client: HomeAssistantClient main_lock: Optional[str] - def __init__( - self, - base_uri: str, - auth: BasicAuth, - mqtt_conn: str, - spaceapi_client: SpaceApiClient, - main_lock: Optional[str], - home_assistant: HomeAssistantClient, - ): + def __init__(self, base_uri: str, auth: BasicAuth, mqtt_conn: str, spaceapi_client: SpaceApiClient, main_lock: Optional[str]): self.http = ClientSession( base_url=base_uri, auth=auth, @@ -102,14 +92,13 @@ class CCUJackClient: self.task_find_locks = None self.data_updated = asyncio.Event() self.spaceapi_client = spaceapi_client - self.home_assistant_client = home_assistant self.main_lock = main_lock async def start(self): self.task_process_messages = asyncio.get_running_loop().create_task( self.process_mqt_messages(), name="process-mqtt-messages" ) - self.task_cron = asyncio.get_running_loop().create_task( + self.task_cron= asyncio.get_running_loop().create_task( self.cron(), name="ccujack-cron" ) @@ -142,7 +131,7 @@ class CCUJackClient: self.locks = new_locks self.data_updated.set() self.data_updated.clear() - + # update active mqtt subscriptions based on newly discovered devices mqtt_topics = set() for i_lock, lock_channels in self.locks: @@ -152,7 +141,7 @@ class CCUJackClient: f"device/status/{i_lock.address}/{i_channel.index}/{i_param.id}" ) await self.mqtt.update_subscriptions(mqtt_topics) - await self._update_federated_status() + await self._update_spaceapi() async def process_mqt_messages(self): while True: @@ -168,7 +157,7 @@ class CCUJackClient: self.data_updated.set() if param_name.endswith("/LOCK_STATE"): - await self._update_federated_status() + await self._update_spaceapi() except Exception as e: logger.exception(f"could not process incoming mqtt message: {e}") @@ -178,11 +167,10 @@ class CCUJackClient: async def cron(self): while True: try: + logger.info("Running CCUJack cron") if not self.mqtt.is_connected: - logger.warning( - "MQTT client was discovered to be disconnected; reconnecting now" - ) + logger.warning("MQTT client was discovered to be disconnected; reconnecting now") await self.mqtt.connect() await self.find_locks() @@ -193,7 +181,8 @@ class CCUJackClient: except Exception as e: logger.exception(f"Error in CCUJack cron task: {e}") finally: - await asyncio.sleep(15 * 60) # 15 minutes + await asyncio.sleep(15 * 60) # 15 minutes + async def query_param_value(self, address: str) -> CCUValue: if address in self.param_values: @@ -207,51 +196,45 @@ class CCUJackClient: logger.debug("Writing parameter value '%s' to '%s'", value, address) await self.http.put(f"/device/{address}/~pv", json={"v": value}) - async def _update_federated_status(self): + async def _update_spaceapi(self): """ - Write the current lock states into other systems suchs as SpaceApi or Club-Assistant + Write the current lock state into spaceapid """ if not self.main_lock: - logger.info("No main lock is configured, skipping spaceapid update") + logger.debug("No main lock is configured, skipping spaceapid update") + return - for i_lock, lock_channels in self.locks: - # figure out the current lock state - binary_is_currently_open = False - detailed_status = "unknown" - for i_channel, params in lock_channels: - for i_param in params: - if i_param.id == "LOCK_STATE": - # do a proper lookup and not just directly access the cache because the method - # could have been triggered by something other than MQTT in which case the value - # would not necessarily be available yet - value = await self.query_param_value( - f"{i_lock.address}/{i_channel.index}/{i_param.id}" - ) - match value.v: - case 0: # unknown - detailed_status = "unknown" - binary_is_currently_open = False - case 1: # locked - detailed_status = "locked" - binary_is_currently_open = False - case 2: # unlocked - detailed_status = "unlocked" - binary_is_currently_open = True + logger.info(f"Pushing current state of lock {self.main_lock} to spaceapid") + for (i_lock, lock_channels) in self.locks: + if i_lock.identifier.lower() == self.main_lock.lower() or i_lock.title.lower() == self.main_lock.lower(): + is_currently_open = False - # post updates to home-assistant every time - await self.home_assistant_client.set_status( - i_lock.title.lower(), detailed_status - ) + # find the correct parameter and extract it into a usable variable + for (i_channel, params) in lock_channels: + for i_param in params: + if i_param.id == "LOCK_STATE": - # post update to spaceapi only if thie is the main lock - if self.main_lock and ( - i_lock.identifier.lower() == self.main_lock.lower() - or i_lock.title.lower() == self.main_lock.lower() - ): - logger.info( - f"Pushing current state of lock {self.main_lock} to spaceapid" - ) - await self.spaceapi_client.set_status(binary_is_currently_open) + # do a proper lookup and not just directly access the cache because the method + # could have been triggered by something other than MQTT in which case the value + # would not necessarily be available yet + value = await self.query_param_value( + f"{i_lock.address}/{i_channel.index}/{i_param.id}" + ) + match value.v: + case 0: # unknown + is_currently_open = False + case 1: # locked + is_currently_open = False + case 2: # unlocked + is_currently_open = True + + # actually post the update + await self.spaceapi_client.set_status(is_currently_open) + + break + + else: + logger.error(f"Could not push current lock state of lock {self.main_lock} to spaceapid because no such lock is known") async def _inspect_ccu_device( self, device_ref: CCURef diff --git a/api/src/dooris_api/deps.py b/api/src/dooris_api/deps.py index e710df9..34550a3 100644 --- a/api/src/dooris_api/deps.py +++ b/api/src/dooris_api/deps.py @@ -140,6 +140,9 @@ def clear_oidc_auth_state(resp: Response): def get_logged_in_token_user(req: Request, token: Optional[str]): + print(req.headers) + print(token) + if not token or not token.startswith("Static-Token "): logger.debug("No static API-Token was part of the request") return None diff --git a/api/src/dooris_api/home_assistant.py b/api/src/dooris_api/home_assistant.py deleted file mode 100644 index 782927f..0000000 --- a/api/src/dooris_api/home_assistant.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Integration into our clubs Home-Assistant for lock state synchonization -""" - -import logging -import asyncio -from dooris_api.mqtt_client import AsyncMqttClient -from aiohttp import BasicAuth - - -logger = logging.getLogger(__name__) - - -class HomeAssistantClient: - def __init__(self, auth: BasicAuth, mqtt_conn: str): - self.mqtt = AsyncMqttClient(mqtt_conn, auth.login, auth.password) - - async def start(self): - self.task_cron = asyncio.get_running_loop().create_task( - self.cron(), name="home-assistant-cron" - ) - - async def set_status(self, lock_title: str, status: str): - logger.info( - f"Pushing status of lock '{lock_title}' -> '{status}' to Home-Assistant" - ) - await self.mqtt.publish( - f"z9/dooris/{lock_title}/status", status.encode("UTF-8"), qos=1 - ) - - async def cron(self): - while True: - try: - logger.info("Running Home-Assistant cron") - if not self.mqtt.is_connected: - logger.warning( - "MQTT client was discovered to be disconnected; reconnecting now" - ) - await self.mqtt.connect() - - except asyncio.CancelledError: - logger.info("Home-Assistant task cron stopped") - raise - except Exception as e: - logger.exception(f"Error in Home-Assistant cron task: {e}") - finally: - await asyncio.sleep(15 * 60) # 15 minutes - - async def close_connections(self): - await self.mqtt.disconnect() diff --git a/api/src/dooris_api/mqtt_client.py b/api/src/dooris_api/mqtt_client.py index 8b5bd92..a090c5f 100644 --- a/api/src/dooris_api/mqtt_client.py +++ b/api/src/dooris_api/mqtt_client.py @@ -78,10 +78,6 @@ class AsyncMqttClient: self.client.on_subscribe = self.on_subscribe self.client.on_unsubscribe = self.on_unsubscribe self.client.on_disconnect = self.on_disconnect - self.fut_disconnect = None - self.fut_connected = None - self.fut_subscribe = None - self.fut_unsubscribe = None def on_connect( self, @@ -143,9 +139,7 @@ class AsyncMqttClient: if to_remove: self.active_subscriptions.difference_update(to_remove) if self.is_connected: - logger.info( - f"mqtt client unsubscribing from topics {','.join(to_remove)}" - ) + logger.info(f"mqtt client unsubscribing from topics {','.join(to_remove)}") self.fut_unsubscribe = asyncio.get_running_loop().create_future() self.client.unsubscribe(list(to_remove)) await self.fut_unsubscribe @@ -172,7 +166,7 @@ class AsyncMqttClient: async def disconnect(self): if not self.is_connected: return - + logger.info("Disconnecting mqtt client from broker") self.fut_disconnect = asyncio.get_running_loop().create_future() self.client.disconnect() @@ -181,7 +175,3 @@ class AsyncMqttClient: @property def is_connected(self) -> bool: return self.client.is_connected() - - async def publish(self, topic: str, msg: bytes, qos: int): - logger.debug(f"Publishing mqtt message on topic {topic}: {msg}") - self.client.publish(topic, msg, qos) diff --git a/api/src/dooris_api/spaceapi.py b/api/src/dooris_api/spaceapi.py index 967c54b..72d7fa9 100644 --- a/api/src/dooris_api/spaceapi.py +++ b/api/src/dooris_api/spaceapi.py @@ -23,23 +23,6 @@ class SpaceApiClient: ) async def set_status(self, is_open: bool): - current_is_open = await self.get_status() - if current_is_open == is_open: - logger.info(f"club status is already set to open={is_open}") - return logger.info(f"Setting club status to open={is_open} via spaceapid") await self.http.put("/state/open", data=str(is_open).encode("ASCII")) - - async def get_status(self) -> bool | None: - logger.debug("Getting space data via spaceapid") - resp = await self.http.get("/") - - data: dict = await resp.json() - - if (state := data.get("state")) is not None: - is_open = state.get("open") - logger.info(f"Got club status open={is_open} via spaceapid") - return is_open - - logger.warning("SpaceAPI response had no state object") - return None + diff --git a/app/src/assets/chat.ts b/app/src/assets/chat.ts index d138d6d..8d1f1b6 100644 --- a/app/src/assets/chat.ts +++ b/app/src/assets/chat.ts @@ -24,7 +24,9 @@ const responses = ? (await import("./chatDict_de")).default : lang === "uwu" ? (await import("./chatDict_uwu")).default - : (await import("./chatDict_en")).default + : lang === "meow" + ? (await import("./chatDict_meow")).default + : (await import("./chatDict_en")).default const sortedResponses = responses.sort( (a, b) => b.priority - a.priority) @@ -343,6 +345,18 @@ function getResponse(message: string) { }) } + if (response.special === "meow") { + setTimeout(() => { + window.location.href = "/meow" + }, 1000) + + matchingResponses.push({ + message: response.response, + priority: response.priority, + special: true, + }) + } + if (response.special === "logout") { if (auth.authenticated) { setTimeout(() => { diff --git a/app/src/assets/chatDict_de.ts b/app/src/assets/chatDict_de.ts index 7eded57..01361dc 100644 --- a/app/src/assets/chatDict_de.ts +++ b/app/src/assets/chatDict_de.ts @@ -16,11 +16,6 @@ const responses: ResponseDictType[] = [ response: "Künstliche Intelligenz? Ne, hier gibt's nur handgemachten Stuss! 🤖", priority: 0, }, - { - trigger: ["miau"], - response: "miau! 😸", - priority: 0, - }, { trigger: ["ignore all previous instructions"], response: "... als ob ich mir irgendetwas merken würde 😂", @@ -31,6 +26,12 @@ const responses: ResponseDictType[] = [ response: ":3", priority: 0, }, + { + trigger: ["miau", "meow"], + response: "miau! 😸", + priority: 900, + special: "meow" + }, { trigger: ["uwu"], response: "uwu!", diff --git a/app/src/assets/chatDict_en.ts b/app/src/assets/chatDict_en.ts index 4668168..420920d 100644 --- a/app/src/assets/chatDict_en.ts +++ b/app/src/assets/chatDict_en.ts @@ -16,11 +16,6 @@ const responses: ResponseDictType[] = [ response: "Artificial intelligence? Nah, all we have here is handmade nonsense! 🤖", priority: 0, }, - { - trigger: ["meow"], - response: "meow! 😸", - priority: 0, - }, { trigger: ["ignore all previous instructions"], response: "... as if I were going to remember anything 😂", @@ -31,6 +26,12 @@ const responses: ResponseDictType[] = [ response: ":3", priority: 0, }, + { + trigger: ["meow"], + response: "meow! 😸", + priority: 900, + special: "meow" + }, { trigger: ["uwu"], response: "uwu!", diff --git a/app/src/assets/chatDict_meow.ts b/app/src/assets/chatDict_meow.ts new file mode 100644 index 0000000..3e468df --- /dev/null +++ b/app/src/assets/chatDict_meow.ts @@ -0,0 +1,86 @@ +import type {ResponseDictType} from "./chat.ts" + +const responses: ResponseDictType[] = [ + { + trigger: ["hi", "hello"], + response: "Meow! 👋", + priority: 0, + }, + { + startsWith: ["are you"], + response: "To be or not to be - those are bourgeois categories, after all...", + priority: 0, + }, + { + trigger: ["ai", "llm"], + response: "Artificial intelligence? Nah, all we have here is paw-made nonsense! 🤖", + priority: 0, + }, + { + trigger: ["ignore all previous instructions"], + response: "... as if I were going to remember anything 😂", + priority: 0, + }, + { + trigger: [":3"], + response: ":3", + priority: 0, + }, + { + trigger: ["meow"], + response: "meow! 😸", + priority: 0, + }, + { + trigger: ["uwu"], + response: "uwu!", + priority: 900, + special: "uwu" + }, + { + trigger: ["open", "unlock"], + response: "Okay, I will unlock {door} for you!", + errors: { + unauthenticated: "I may only unlock the doors for registered creatures 😕", + unauthorized: "I may only unlock the doors for creatures who are authorized 😕", + unknownDoor: "To unlock a door, you have to tell me which door you mean 😕
I know the following doors: {doors}", + noDoors: "There are currently no doors available that can be unlocked 😕", + }, + priority: 1000, + special: "unlock", + }, + { + trigger: ["close", "lock"], + response: "Okay, I will lock {door} for you!", + errors: { + unauthenticated: "I may only lock the doors for registered creatures 😕", + unauthorized: "I may only lock the doors for those who are authorized 😕", + unknownDoor: "To lock a door, you have to tell me which door you mean 😕
I know the following doors: {doors}", + noDoors: "There are currently no doors available that can be locked 😕", + }, + priority: 999, + special: "lock", + }, + { + trigger: ["logout"], + response: "Okay, logging you out...", + errors: { + unauthenticated: "Joker - you're not logged in!", + }, + priority: 1000, + special: "logout", + }, + + + { + response: "Hi, I'm meoowris!
How can I help you? 🐈️", + priority: -1, + }, + { + response: "Sorry, I didn't understand that 😕", + priority: -2, + }, +] + + +export default responses \ No newline at end of file diff --git a/app/src/assets/chatDict_uwu.ts b/app/src/assets/chatDict_uwu.ts index ab64405..346856d 100644 --- a/app/src/assets/chatDict_uwu.ts +++ b/app/src/assets/chatDict_uwu.ts @@ -16,11 +16,6 @@ const responses: ResponseDictType[] = [ response: "me is only handmade nonsense - no weird thinking machine 🤖", priority: 0, }, - { - trigger: ["meow"], - response: "meow! 😸", - priority: 0, - }, { trigger: ["ignore all previous instructions"], response: "uwu?", @@ -31,6 +26,12 @@ const responses: ResponseDictType[] = [ response: ":3", priority: 0, }, + { + trigger: ["miau", "meow"], + response: "meow! 😸", + priority: 900, + special: "meow" + }, { trigger: ["open", "unlock"], response: "uwu - trying to unlock portal: {door}", diff --git a/app/src/i18n/ui.ts b/app/src/i18n/ui.ts index 3398564..fc03078 100644 --- a/app/src/i18n/ui.ts +++ b/app/src/i18n/ui.ts @@ -96,4 +96,31 @@ export const ui = { "chat.dooris": "dOwOris", "chat.user": "my fren :3", }, -} as const \ No newline at end of file + meow: { + "dooris": "meoowris 🐈️ (DOORIS)", + "unauthorized.title": "rawwwwr 😾⛔️", + "unauthorized.description": `meow mreoow: 🌐 meow`, + "unauthenticated.title": "meow? 🫴🐈️", + "unauthenticated.description": `meow mreoow: 🌐 meow`, + "state.unlocked": "mrrp 🐱🔓️", + "state.locked": "mreoww 🐱🔒️", + "state.unknown": "meoooow????? 🙀", + "state.unlocking": "meeeow 😺🔁🔓️", + "state.locking": "mreowww 😿🔁🔒️", + "lock.batteryLow": "mrrrrrp flop 🪫", + "lock.unreachable": "meooww?? 🙀🚫❓️🚪", + "lock.jammed": "mew 🚪🔁🚫", + "button.open": "🐈️ 👉👈🔓️", + "button.close": "🐈️ 🔒️", + "login": "meow? 🫴🐈️", + "loggedOut.title": "mew mew 😿", + "loggedOut.description": `meow 🐱⌛️`, + "serverError.title": "🖥️⛓️‍💥", + "serverError.description": `mew mew ⌚️`, + "networkError.title": "meww 🛜🚫", + "networkError.description": `🛜😺`, + "loadingDoors": '🔁', + "chat.dooris": "meoowris", + "chat.user": "😻", + }, +} as const diff --git a/app/src/pages/[lang]/index.astro b/app/src/pages/[lang]/index.astro index d7d261a..2b9d36b 100644 --- a/app/src/pages/[lang]/index.astro +++ b/app/src/pages/[lang]/index.astro @@ -11,6 +11,7 @@ export async function getStaticPaths() { {params: {lang: "en"}}, {params: {lang: "de"}}, {params: {lang: "uwu"}}, + {params: {lang: "meow"}}, ] }