diff --git a/pyproject.toml b/pyproject.toml
index cc84391..6cb0070 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,8 +33,10 @@ dependencies = [
"PyJWT",
"multiavatar",
"gitpython",
- "uvloop",
- "humanize"
+ 'uvloop; platform_system != "Windows"',
+ "humanize",
+ "Pillow",
+ "pillow-heif",
]
[tool.setuptools.packages.find]
diff --git a/src/snek/__main__.py b/src/snek/__main__.py
index 35e56e3..0f06499 100644
--- a/src/snek/__main__.py
+++ b/src/snek/__main__.py
@@ -1,5 +1,4 @@
import click
-import uvloop
from aiohttp import web
import asyncio
from snek.app import Application
@@ -14,7 +13,12 @@ def cli():
@click.option('--host', default='0.0.0.0', show_default=True, help='Host to run the application on')
@click.option('--db_path', default='snek.db', show_default=True, help='Database path for the application')
def serve(port, host, db_path):
- asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
+ try:
+ import uvloop
+ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
+ except ImportError:
+ print("uvloop not installed, using default event loop.")
+
web.run_app(
Application(db_path=f"sqlite:///{db_path}"), port=port, host=host
)
diff --git a/src/snek/sssh.py b/src/snek/sssh.py
index 0106a17..848b2f9 100644
--- a/src/snek/sssh.py
+++ b/src/snek/sssh.py
@@ -1,10 +1,6 @@
-import asyncio
import asyncssh
import logging
-import os
from pathlib import Path
-import sys
-import pty
global _app
diff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py
index 82a222e..b708666 100644
--- a/src/snek/system/markdown.py
+++ b/src/snek/system/markdown.py
@@ -4,6 +4,9 @@ from types import SimpleNamespace
from app.cache import time_cache_async
from mistune import HTMLRenderer, Markdown
+from mistune.plugins.formatting import strikethrough
+from mistune.plugins.spoiler import spoiler
+from mistune.plugins.url import url
from pygments import highlight
from pygments.formatters import html
from pygments.lexers import get_lexer_by_name
@@ -14,6 +17,8 @@ class MarkdownRenderer(HTMLRenderer):
_allow_harmful_protocols = True
def __init__(self, app, template):
+ super().__init__(False, True)
+
self.template = template
self.app = app
@@ -46,10 +51,18 @@ class MarkdownRenderer(HTMLRenderer):
markdown = Markdown(renderer=renderer)
return markdown(markdown_string)
+ # def image(self, text: str, url: str, title: Optional[str] = None) -> str:
+ # src = self.safe_url(url)
+ # alt = escape(striptags(text))
+ # s = '
'
+
def render_markdown_sync(app, markdown_string):
renderer = MarkdownRenderer(app, None)
- markdown = Markdown(renderer=renderer)
+ markdown = Markdown(renderer=renderer, plugins=[url, strikethrough, spoiler])
return markdown(markdown_string)
diff --git a/src/snek/system/template.py b/src/snek/system/template.py
index d4b6819..93fd33c 100644
--- a/src/snek/system/template.py
+++ b/src/snek/system/template.py
@@ -1,6 +1,7 @@
import re
from types import SimpleNamespace
+import mimetypes
import emoji
from bs4 import BeautifulSoup
from jinja2 import TemplateSyntaxError, nodes
@@ -105,21 +106,38 @@ def embed_youtube(text):
def embed_image(text):
soup = BeautifulSoup(text, "html.parser")
for element in soup.find_all("a"):
- for extension in [
- ".png",
- ".jpg",
- ".jpeg",
- ".gif",
- ".webp",
- ".svg",
- ".bmp",
- ".tiff",
- ".ico",
- ".heif",
- ]:
- if extension in element.attrs["href"].lower():
- embed_template = f'
'
- element.replace_with(BeautifulSoup(embed_template, "html.parser"))
+ file_mime = mimetypes.guess_type(element.attrs["href"])[0]
+
+ if file_mime and file_mime.startswith("image/") or any(
+ ext in element.attrs["href"].lower() for ext in [
+ ".png",
+ ".jpg",
+ ".jpeg",
+ ".gif",
+ ".webp",
+ ".svg",
+ ".bmp",
+ ".tiff",
+ ".ico",
+ ".heif",
+ ".heic",
+ ]
+ ):
+ embed_template = f'
'
+ element.replace_with(BeautifulSoup(embed_template, "html.parser"))
+ return str(soup)
+
+def enrich_image_rendering(text):
+ soup = BeautifulSoup(text, "html.parser")
+ for element in soup.find_all("img"):
+ if element.attrs["src"].startswith("/" ):
+ picture_template = f'''
+
+
+
+
+ '''
+ element.replace_with(BeautifulSoup(picture_template, "html.parser"))
return str(soup)
@@ -205,6 +223,8 @@ class LinkifyExtension(Extension):
result = embed_media(result)
result = embed_image(result)
result = embed_youtube(result)
+
+ result = enrich_image_rendering(result)
return result
diff --git a/src/snek/templates/web.html b/src/snek/templates/web.html
index 0810587..1046d2e 100644
--- a/src/snek/templates/web.html
+++ b/src/snek/templates/web.html
@@ -49,7 +49,7 @@
})
document.querySelector("upload-button").addEventListener("uploaded",function(e){
e.detail.files.forEach((file)=>{
- app.rpc.sendMessage(channelUid,``)
+ app.rpc.sendMessage(channelUid,`[${file.name}](/channel/attachment/${file.relative_url})`)
})
})
textBox.addEventListener("paste", async (e) => {
diff --git a/src/snek/view/channel.py b/src/snek/view/channel.py
index 93ad412..86ed7a0 100644
--- a/src/snek/view/channel.py
+++ b/src/snek/view/channel.py
@@ -1,27 +1,100 @@
+import asyncio
+import mimetypes
+
+from PIL import Image
+import pillow_heif.HeifImagePlugin
+
from snek.system.view import BaseView
-import aiofiles
+import aiofiles
from aiohttp import web
-import pathlib
+import pathlib
+
class ChannelAttachmentView(BaseView):
-
async def get(self):
relative_path = self.request.match_info.get("relative_url")
- channel_attachment = await self.services.channel_attachment.get(relative_url=relative_path)
- response = web.FileResponse(channel_attachment["path"])
- response.headers["Cache-Control"] = f"public, max-age={1337*420}"
- response.headers["Content-Disposition"] = (
- f'attachment; filename="{channel_attachment["name"]}"'
+ channel_attachment = await self.services.channel_attachment.get(
+ relative_url=relative_path
)
- return response
+
+ current_format = mimetypes.guess_type(channel_attachment["path"])[0]
+
+ format = self.request.query.get("format")
+ width = self.request.query.get("width")
+ height = self.request.query.get("height")
+
+ if any([format, width, height]) and current_format.startswith("image/"):
+ with Image.open(channel_attachment["path"]) as image:
+ response = web.StreamResponse(
+ status=200,
+ reason="OK",
+ headers={
+ "Cache-Control": f"public, max-age={1337 * 420}",
+ "Content-Type": f"image/{format}" if format else current_format,
+ "Content-Disposition": f'attachment; filename="{channel_attachment["name"]}"',
+ },
+ )
+
+ if width or height:
+ width = min(int(width), image.size[0]) if width else None
+ height = min(int(height), image.size[1]) if height else None
+
+ if width and height:
+ smallest_ratio = max(
+ image.size[0] / int(width), image.size[1] / int(height)
+ )
+ image.thumbnail(
+ (
+ int(image.size[0] / smallest_ratio),
+ int(image.size[1] / smallest_ratio),
+ )
+ )
+ elif width:
+ image.thumbnail(
+ (
+ int(width),
+ int(image.size[1] * image.size[0] / int(width)),
+ )
+ )
+ elif height:
+ image.thumbnail(
+ (
+ int(image.size[0] * image.size[1] / int(height)),
+ int(height),
+ )
+ )
+
+ await response.prepare(self.request)
+
+ # response.write is async but image.save is not
+ naughty_steal = response.write
+ loop = asyncio.get_event_loop()
+
+ def sync_writer(*args, **kwargs):
+ return loop.run_until_complete(naughty_steal(*args, **kwargs))
+
+ setattr(response, "write", sync_writer)
+
+ image.save(response, format=self.request.query["format"])
+
+ setattr(response, "write", naughty_steal)
+ return response
+ else:
+ response = web.FileResponse(channel_attachment["path"])
+ response.headers["Cache-Control"] = f"public, max-age={1337 * 420}"
+ response.headers["Content-Disposition"] = (
+ f'attachment; filename="{channel_attachment["name"]}"'
+ )
+ return response
async def post(self):
-
channel_uid = self.request.match_info.get("channel_uid")
user_uid = self.request.session.get("uid")
- channel_member = await self.services.channel_member.get(user_uid=user_uid, channel_uid=channel_uid,deleted_at=None,is_banned=False)
-
+ channel_member = await self.services.channel_member.get(
+ user_uid=user_uid, channel_uid=channel_uid, deleted_at=None, is_banned=False
+ )
+
if not channel_member:
return web.HTTPNotFound()
@@ -29,18 +102,17 @@ class ChannelAttachmentView(BaseView):
attachments = []
while field := await reader.next():
-
filename = field.filename
if not filename:
continue
attachment = await self.services.channel_attachment.create_file(
- channel_uid=channel_uid, name=filename,user_uid=user_uid
+ channel_uid=channel_uid, name=filename, user_uid=user_uid
)
-
+
attachments.append(attachment)
- pathlib.Path(attachment['path']).parent.mkdir(parents=True, exist_ok=True)
- async with aiofiles.open(attachment['path'], "wb") as f:
+ pathlib.Path(attachment["path"]).parent.mkdir(parents=True, exist_ok=True)
+ async with aiofiles.open(attachment["path"], "wb") as f:
while chunk := await field.read_chunk():
await f.write(chunk)