Skip to content

Commit

Permalink
holy shit its 0.11
Browse files Browse the repository at this point in the history
  • Loading branch information
iiPythonx committed Nov 27, 2024
1 parent a16f49b commit 17dc329
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 69 deletions.
2 changes: 1 addition & 1 deletion nightwatch/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.10.7"
__version__ = "0.11.0"

import re
HEX_COLOR_REGEX = re.compile(r"^[A-Fa-f0-9]{6}$")
41 changes: 3 additions & 38 deletions nightwatch/rics/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
# Copyright (c) 2024 iiPython

# Modules
import base64
import typing
import binascii
from time import time
from json import JSONDecodeError
from secrets import token_urlsafe

from requests import Session, RequestException
from pydantic import BaseModel, Field

from fastapi import FastAPI, Response, WebSocket
from fastapi import FastAPI, WebSocket
from fastapi.responses import JSONResponse
from starlette.websockets import WebSocketDisconnect, WebSocketState

Expand Down Expand Up @@ -183,40 +180,8 @@ 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."}})

# Handle image forwarding
SESSION = Session()
PROXY_SIZE_LIMIT = 10 * (1024 ** 2)

@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", "Cache-Control"]
}
)

except RequestException:
return JSONResponse({"code": 400, "message": "Failed to contact the specified URI."}, status_code = 400)

# Load additional routes
from nightwatch.rics.routing import ( # noqa: E402
files # noqa: F401
files, # noqa: F401
image_forward # noqa: F401
)
60 changes: 60 additions & 0 deletions nightwatch/rics/routing/image_forward.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright (c) 2024 iiPython

# Modules
import os
import base64
import binascii

from fastapi import Response
from fastapi.responses import JSONResponse
from requests import Session, RequestException

from nightwatch.rics import app
from nightwatch.logging import log

# Exceptions
class IllegalURL(Exception):
pass

# Handle image forwarding
SESSION = Session()
PROXY_SIZE_LIMIT = 10 * (1024 ** 2)

FORWARD_DOMAIN = os.getenv("DOMAIN")
if FORWARD_DOMAIN is None:
log.warn("images", "DOMAIN environment variable not set! Image forwarding unprotected!")

# Routing
@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('/')}"
if FORWARD_DOMAIN and FORWARD_DOMAIN in new_url:
raise IllegalURL

except (binascii.Error, UnicodeDecodeError):
return JSONResponse({"code": 400, "message": "Failed to contact the specified URI."}, status_code = 400)

except IllegalURL:
return JSONResponse({"code": 400, "message": "Requested URL contains an illegal string!"}, 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", "Cache-Control"]
}
)

except RequestException:
return JSONResponse({"code": 400, "message": "Failed to contact the specified URI."}, status_code = 400)
58 changes: 28 additions & 30 deletions nightwatch/web/js/nightwatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ const FILE_HANDLER = new FileHandler();

// Construct text/attachment
let attachment = message.message, classlist = "message-content";
let raw_attachment = attachment;
if (attachment.toLowerCase().match(/^https:\/\/[\w\d./-]+.(?:avifs?|a?png|jpe?g|jfif|webp|ico|gif|svg)(?:\?.+)?$/)) {

const file_match = attachment.match(new RegExp(`^https?:\/\/${address}\/file\/([a-zA-Z0-9_-]{21})\/.*$`));
if (!file_match && attachment.toLowerCase().match(/^https:\/\/[\w\d./-]+.(?:avifs?|a?png|jpe?g|jfif|webp|ico|gif|svg)(?:\?.+)?$/)) {
attachment = `![untitled](${attachment})`;
}

// Clean attachment for the love of god
const cleaned = attachment.replace(/&/g, "&")
.replace(/</g, "&lt;")
Expand All @@ -119,34 +119,12 @@ const FILE_HANDLER = new FileHandler();
};
};

// Check for files
const file_match = raw_attachment.match(new RegExp(`^https?:\/\/${address}\/file\/([a-zA-Z0-9_-]{21})\/.*$`));
if (file_match) {
function bytes_to_human(size) {
const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return +((size / Math.pow(1024, i)).toFixed(2)) * 1 + " " + ["B", "kB", "MB", "GB"][i];
}
const response = await (await fetch(`http${connection.protocol}://${address}/api/file/${file_match[1]}/info`)).json();
if (response.code === 200) {
const mimetype = FILE_HANDLER.mimetype(response.data.name);
if (["avif", "avifs", "png", "apng", "jpg", "jpeg", "jfif", "webp", "ico", "gif", "svg"].includes(mimetype.toLowerCase())) {
attachment = `<a href = "${attachment}" target = "_blank"><img alt = "${response.data.name}" src = "${attachment}"></a>`;
} else {
attachment = `<div class = "file">
<div><span>${response.data.name}</span> <span>${mimetype}</span></div>
<div><span>${bytes_to_human(response.data.size)}</span> <button data-uri="${attachment}">Download</button></div>
</div>`;
}
classlist += " padded";
}
}

// Construct message
const element = document.createElement("div");
element.classList.add("message");
element.innerHTML = `
<span style = "color: #${message.user.hex};${hide_author ? 'color: transparent;' : ''}">${message.user.name}</span>
<span class = "${classlist}">${attachment}</span>
<span class = "${classlist}">${file_match ? "Loading attachment..." : attachment}</span>
<span class = "timestamp"${current_time === last_time ? ' style="color: transparent;"' : ''}>${current_time}</span>
`;

Expand All @@ -156,12 +134,32 @@ const FILE_HANDLER = new FileHandler();
chat.scrollTop = chat.scrollHeight;
last_author = message.user.name, last_time = current_time;

// Handle downloading
const button = element.querySelector("[data-uri]");
if (button) button.addEventListener("click", () => { window.open(button.getAttribute("data-uri"), "_blank"); });

// Handle notification sound
if (!document.hasFocus()) NOTIFICATION_SFX.play();

// Check for files
if (file_match) {
function bytes_to_human(size) {
const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return +((size / Math.pow(1024, i)).toFixed(2)) * 1 + " " + ["B", "kB", "MB", "GB"][i];
}

const response = await (await fetch(`http${connection.protocol}://${address}/api/file/${file_match[1]}/info`)).json();
if (response.code === 200) {
const message = element.querySelector(".message-content");
const mimetype = FILE_HANDLER.mimetype(response.data.name);
if (["avif", "avifs", "png", "apng", "jpg", "jpeg", "jfif", "webp", "ico", "gif", "svg"].includes(mimetype.toLowerCase())) {
message.innerHTML = `<a href = "${attachment}" target = "_blank"><img alt = "${response.data.name}" src = "${attachment}"></a>`;
} else {
message.innerHTML = `<div class = "file">
<div><span>${response.data.name}</span> <span>${mimetype}</span></div>
<div><span>${bytes_to_human(response.data.size)}</span> <button>Download</button></div>
</div>`;
message.querySelector("button").addEventListener("click", () => { window.open(attachment, "_blank"); });
}
message.classList.add("padded");
}
}
},
handle_member: (event_type, member) => {
const member_list = document.querySelector(".member-list");
Expand Down

0 comments on commit 17dc329

Please sign in to comment.