From e6596bccf376bb83a14e5bf7324e1afd94cb5ce6 Mon Sep 17 00:00:00 2001 From: iiPython Date: Mon, 25 Nov 2024 21:32:04 -0600 Subject: [PATCH] fix backend when it comes to images --- nextgen_bot.py | 145 ++++++++++++++++++++++++++++++++ nightwatch/__init__.py | 2 +- nightwatch/rics/__init__.py | 14 ++- nightwatch/web/css/main.css | 4 + nightwatch/web/js/nightwatch.js | 39 +++++---- 5 files changed, 179 insertions(+), 25 deletions(-) create mode 100644 nextgen_bot.py diff --git a/nextgen_bot.py b/nextgen_bot.py new file mode 100644 index 0000000..213eab4 --- /dev/null +++ b/nextgen_bot.py @@ -0,0 +1,145 @@ +# Copyright (c) 2024 iiPython + +# Modules +import typing +from base64 import b64encode +from urllib.parse import quote_plus +from platform import python_version + +from requests import Session + +from nightwatch import __version__ +from nightwatch.bot import Client, Context +from nightwatch.bot.client import AuthorizationFailed + +# Handle now playing +session = Session() + +def get_spotify_access() -> str: + with session.post( + "https://accounts.spotify.com/api/token", + data = "grant_type=client_credentials", + headers = { + "Authorization": f"Basic {b64encode(b'3f974573800a4ff5b325de9795b8e603:ff188d2860ff44baa57acc79c121a3b9').decode()}", + "Content-Type": "application/x-www-form-urlencoded" + } + ) as response: + return response.json()["access_token"] + +def get_now_playing() -> tuple[dict | None, str | None]: + with session.get("https://api.listenbrainz.org/1/user/iiPython/playing-now") as response: + result = (response.json())["payload"]["listens"] + + if not result: + return None, None + + result = result[0] + + # Reorganize the result data + tm = result["track_metadata"] + result = {"artist": tm["artist_name"], "track": tm["track_name"], "album": tm["release_name"],} + with session.get( + f"https://api.spotify.com/v1/search?q={quote_plus(f'{result['artist']} {result['album']}')}&type=album&limit=1", + headers = { + "Authorization": f"Bearer {get_spotify_access()}", + "Content-Type": "application/x-www-form-urlencoded" + } + ) as response: + images = (response.json())["albums"]["items"] + return result, images[0]["images"][-1]["url"] + +# Helping methods +def dominant_color(hex: str) -> str: + r, g, b = tuple(int(hex[i:i + 2], 16) for i in (0, 2, 4)) + if r > g and r > b: + return "red(ish)" + + elif g > r and g > b: + return "green(ish)" + + elif b > r and b > g: + return "blue(ish)" + + elif r == g and r > b: + return "yellow(ish)" + + elif r == b and r > g: + return "magenta(ish)" + + elif g == b and g > r: + return "cyan(ish)" + + return "gray(ish)" + +# Create client +class NextgenerationBot(Client): + def __init__(self) -> None: + super().__init__() + + # Extra data + self.send_on_join = None + + async def rejoin(self, username: typing.Optional[str] = None, hex: typing.Optional[str] = None) -> None: + await self.close() + await self.event_loop(username or self.user.name, hex or self.user.hex, self.address) # type: ignore + + async def on_connect(self, ctx: Context) -> None: + print(f"Connected to '{ctx.rics.name}'!") + + async def on_message(self, ctx: Context) -> None: + if self.send_on_join is not None: + await ctx.send(self.send_on_join) + self.send_on_join = None + return + + command = ctx.message.message + if command[0] != "/": + return + + match command[1:].split(" "): + case ["help"]: + await ctx.reply("Commands: /help, /music, /user, /people, /rename, /set-hex, /version") + + case ["music"]: + data, image = get_now_playing() + if not (data and image): + return await ctx.reply("iiPython isn't listening to anything right now.") + + await ctx.send(f"iiPython is listening to {data['track']} by {data['artist']} (on {data['album']}).") + await ctx.send(f"![{data['track']} by {data['artist']} cover art]({image})") + + case ["user", *username]: + client = next(filter(lambda u: u.name == " ".join(username), ctx.rics.users), None) + if client is None: + return await ctx.reply("Specified user doesn't *fucking* exist.") + + await ctx.send(f"**Name:** {'🤖 ' if client.bot else '★ ' if client.admin else ''}{client.name} | **HEX Code:** #{client.hex} [{dominant_color(client.hex)}]") + + case ["rename" | "set-hex" as command, *response]: + try: + await self.rejoin( + " ".join(response) if command == "rename" else None, + response[0] if command == "set-hex" else None + ) + + except AuthorizationFailed as problem: + if problem.json is not None: + message = (problem.json.get("message") or problem.json["detail"][0]["msg"]).rstrip(".").lower() + self.send_on_join = f"Failed to switch {'username' if command == 'rename' else 'hex code'} because '{message}'." + + await self.event_loop(self.user.name, self.user.hex, self.address) # type: ignore + + case ["people"]: + await ctx.send(f"There are {len(ctx.rics.users)} users: {', '.join(f'{u.name}{f' ({'admin' if u.admin else 'bot'})' if u.admin or u.bot else ''}' for u in ctx.rics.users)}") + + case ["version"]: + await ctx.reply(f"Running on Nightwatch v{__version__} using Python {python_version()}.") + + case _: + await ctx.reply("I have **no idea** what the *fuck* you just asked...") + +NextgenerationBot().run( + username = "Pizza Eater", + hex = "ff0000", + address = "nightwatch.k4ffu.dev" +) diff --git a/nightwatch/__init__.py b/nightwatch/__init__.py index 4a04c29..b7f3eec 100644 --- a/nightwatch/__init__.py +++ b/nightwatch/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.10.6" +__version__ = "0.10.7" import re HEX_COLOR_REGEX = re.compile(r"^[A-Fa-f0-9]{6}$") diff --git a/nightwatch/rics/__init__.py b/nightwatch/rics/__init__.py index 88a9ec4..caee7bc 100644 --- a/nightwatch/rics/__init__.py +++ b/nightwatch/rics/__init__.py @@ -162,6 +162,10 @@ async def connect_endpoint( while websocket.application_state == WebSocketState.CONNECTED: match await client.receive(): case {"type": "message", "data": {"message": message}}: + if not message.strip(): + await client.send({"type": "problem", "data": {"message": "You cannot send a blank message."}}) + continue + await app.state.broadcast({"type": "message", "data": {"user": client.serialize(), "message": message}}) case {"type": "user-list", "data": _}: @@ -182,7 +186,6 @@ async def connect_endpoint( # 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: @@ -192,13 +195,6 @@ async def forward_image(public_url: str) -> Response | JSONResponse: except (binascii.Error, UnicodeDecodeError): return JSONResponse({"code": 400, "message": "Failed to contact the specified URI."}, status_code = 400) - filename = new_url.split("?")[0].split("/")[-1] - if "." not in filename: - return JSONResponse({"code": 400, "message": "Specified URI does not have an extension."}, status_code = 400) - - if filename.split(".")[-1] not in PROXY_ALLOWED_SUFFIX: - return JSONResponse({"code": 400, "message": "Specified URI has an unsupported extension."}, status_code = 400) - try: data = b"" with SESSION.get(new_url, stream = True) as response: @@ -213,7 +209,7 @@ async def forward_image(public_url: str) -> Response | JSONResponse: response.status_code, { k: v - for k, v in response.headers.items() if k in ["Content-Type", "Content-Length", "Cache-Control"] + for k, v in response.headers.items() if k in ["Content-Type", "Cache-Control"] } ) diff --git a/nightwatch/web/css/main.css b/nightwatch/web/css/main.css index fa3b86e..264ac2b 100644 --- a/nightwatch/web/css/main.css +++ b/nightwatch/web/css/main.css @@ -103,6 +103,10 @@ input:hover { padding-top: 5px; padding-bottom: 5px; } +.message-content.has-image > span > a { + display: flex; + align-items: center; +} .message-content > a > img { max-width: 500px; } diff --git a/nightwatch/web/js/nightwatch.js b/nightwatch/web/js/nightwatch.js index 9cdf563..8ca48c1 100644 --- a/nightwatch/web/js/nightwatch.js +++ b/nightwatch/web/js/nightwatch.js @@ -9,6 +9,7 @@ const leftmark_rules = [ { regex: /__(.*?)__/g, replace: "$1" }, { regex: /~~(.*?)~~/g, replace: "$1" }, { regex: /\*(.*?)\*/g, replace: "$1" }, + { regex: /\!\[(.*?)\]\((.*?)\)/g, replace: `$1` }, { regex: /\[(.*?)\]\((.*?)\)/g, replace: `$1` } ]; @@ -86,21 +87,29 @@ const NOTIFICATION_SFX = new Audio("/audio/notification.mp3"); // 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)(?:\?.+)?$/)) { - const url = `http${connection.protocol}://${address}/api/fwd/${btoa(attachment.slice(8))}`; - attachment = ``; - classlist += " has-image"; - } else { - - // Clean attachment for the love of god - const cleaned = attachment.replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/"/g, "'"); - - // Apply leftmark - attachment = leftmark(cleaned); - if (cleaned !== attachment) attachment = `${attachment}`; + attachment = `![untitled](${attachment})`; + } + + // Clean attachment for the love of god + const cleaned = attachment.replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/"/g, "'"); + + // Apply leftmark + attachment = leftmark(cleaned); + if (cleaned !== attachment) { + attachment = `${attachment}`; + const dom = new DOMParser().parseFromString(attachment, "text/html"); + + // Handle image adjusting + const image = dom.querySelector("img"); + if (image) { + classlist += " has-image"; + image.src = `http${connection.protocol}://${address}/api/fwd/${btoa(image.src.slice(8))}`; + attachment = dom.body.innerHTML; + }; }; // Construct message