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