diff --git a/README.md b/README.md index fc1bf2c..3c0d6bb 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ git clone https://github.com/iiPythonx/nightwatch && cd nightwatch git checkout release uv venv uv pip install -e . -HOST=0.0.0.0 python3 -m nightwatch.server +uvicorn nightwatch.rics:app --host 0.0.0.0 ``` An example NGINX configuration: @@ -44,14 +44,14 @@ server { # Setup location server_name nightwatch.iipython.dev; - location /proxy { + location /api { proxy_pass http://192.168.0.1:8000; proxy_http_version 1.1; } - location /gateway { + location /api/ws { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; - proxy_pass http://192.168.0.1:8000/gateway; + proxy_pass http://192.168.0.1:8000/api/ws; proxy_http_version 1.1; } } diff --git a/nightwatch/rics/__init__.py b/nightwatch/rics/__init__.py index 9531fe2..39a1f0c 100644 --- a/nightwatch/rics/__init__.py +++ b/nightwatch/rics/__init__.py @@ -1,13 +1,17 @@ # Copyright (c) 2024 iiPython # Modules +import base64 import typing +import binascii from time import time from secrets import token_urlsafe -from fastapi import FastAPI, WebSocket -from fastapi.responses import JSONResponse +from requests import Session, RequestException from pydantic import BaseModel, Field + +from fastapi import FastAPI, Response, WebSocket +from fastapi.responses import JSONResponse from starlette.websockets import WebSocketDisconnect, WebSocketState from nightwatch.config import fetch_config @@ -133,3 +137,37 @@ async def connect_endpoint( await app.state.broadcast({"type": "leave", "data": {"user": client.serialize()}}) await app.state.broadcast({"type": "message", "data": {"message": f"{client.username} has left the server."}}) client.cleanup() + +# Handle image forwarding +SESSION = Session() +PROXY_SIZE_LIMIT = 10 * (1024 ** 2) +PROXY_ALLOWED_SUFFIX = ["avif", "avifs", "apng", "png", "jpeg", "jpg", "jfif", "webp", "ico", "gif", "svg"] + +@app.get("/api/fwd/{public_url:str}", response_model = None) +async def forward_image(public_url: str) -> Response | JSONResponse: + try: + new_url = f"https://{base64.b64decode(public_url, validate = True).decode('ascii').rstrip("/")}" + + except (binascii.Error, UnicodeDecodeError): + return JSONResponse({"code": 400, "message": "Failed to contact the specified URI."}, status_code = 400) + + try: + data = b"" + with SESSION.get(new_url, stream = True) as response: + response.raise_for_status() + for chunk in response.iter_content(PROXY_SIZE_LIMIT): + data += chunk + if len(data) >= PROXY_SIZE_LIMIT: + return JSONResponse({"code": 400, "message": "Specified URI contains data above size limit."}, status_code = 400) + + return Response( + data, + response.status_code, + { + k: v + for k, v in response.headers.items() if k in ["Content-Type", "Content-Length", "Cache-Control"] + } + ) + + except RequestException: + return JSONResponse({"code": 400, "message": "Failed to contact the specified URI."}, status_code = 400) diff --git a/nightwatch/web/js/nightwatch.js b/nightwatch/web/js/nightwatch.js index cb9c058..18edd11 100644 --- a/nightwatch/web/js/nightwatch.js +++ b/nightwatch/web/js/nightwatch.js @@ -81,13 +81,12 @@ const NOTIFICATION_SFX = new Audio("/audio/notification.mp3"); // Check for anything hidden const hide_author = message.user.name === last_author; - const hide_time = !hide_author ? false : current_time === last_time; last_author = message.user.name, last_time = current_time; // Construct text/attachment let attachment = message.message, classlist = "message-content"; if (attachment.toLowerCase().match(/^https:\/\/[\w\d./-]+.(?:avifs?|a?png|jpe?g|jfif|webp|ico|gif|svg)(?:\?.+)?$/)) { - attachment = ``; + attachment = ``; classlist += " has-image"; } else { @@ -109,7 +108,7 @@ const NOTIFICATION_SFX = new Audio("/audio/notification.mp3"); element.innerHTML = ` ${message.user.name} ${attachment} - ${current_time} + ${current_time} `; // Push message and autoscroll