From a41da84e3fd57ad72fd06946352add6236f773e3 Mon Sep 17 00:00:00 2001 From: retoor Date: Thu, 12 Jun 2025 21:59:37 +0200 Subject: [PATCH 01/18] Progress. --- src/snek/static/chat-input.js | 4 ++-- src/snek/static/file-upload-grid.js | 7 ++++--- src/snek/static/upload-button.js | 4 ++-- src/snek/view/channel.py | 9 +++++++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/snek/static/chat-input.js b/src/snek/static/chat-input.js index 0800f61..90f3979 100644 --- a/src/snek/static/chat-input.js +++ b/src/snek/static/chat-input.js @@ -181,8 +181,8 @@ class ChatInputComponent extends NjetComponent { this.dispatchEvent(new CustomEvent("uploaded", e)); }); this.uploadButton.addEventListener("click", (e) => { - // e.preventDefault(); - // this.fileUploadGrid.openFileDialog() + e.preventDefault(); + this.fileUploadGrid.openFileDialog() }) this.subscribe("file-uploading", (e) => { diff --git a/src/snek/static/file-upload-grid.js b/src/snek/static/file-upload-grid.js index 3aae7ba..0a623f6 100644 --- a/src/snek/static/file-upload-grid.js +++ b/src/snek/static/file-upload-grid.js @@ -130,7 +130,8 @@ class FileUploadGrid extends NjetComponent { startUpload(file, tile, progress) { this.publish('file-uploading', {file: file, tile: tile, progress: progress}); - + console.info("File uploading",file) + const protocol = location.protocol === "https:" ? "wss://" : "ws://"; const ws = new WebSocket(`${protocol}${location.host}/channel/${this.channelUid}/attachment.sock`); ws.binaryType = 'arraybuffer'; @@ -148,7 +149,7 @@ class FileUploadGrid extends NjetComponent { }; ws.onmessage = (event) => { - + cconsole.info(event.data) const data = JSON.parse(event.data); if (data.type === 'progress') { @@ -161,7 +162,7 @@ class FileUploadGrid extends NjetComponent { this.publish('file-uploaded', {file: file, tile: tile, progress: progress}); progress.style.width = '100%'; tile.classList.add('fug-done'); - + console.info("Closed") ws.close(); this.reset() diff --git a/src/snek/static/upload-button.js b/src/snek/static/upload-button.js index 243fc8b..a789c96 100644 --- a/src/snek/static/upload-button.js +++ b/src/snek/static/upload-button.js @@ -112,9 +112,9 @@ class UploadButtonElement extends HTMLElement { this.channelUid = this.getAttribute("channel"); this.uploadButton = this.container.querySelector(".upload-button"); this.fileInput = this.container.querySelector(".hidden-input"); - this.uploadButton.addEventListener("click", () => { + /*this.uploadButton.addEventListener("click", () => { this.fileInput.click(); - }); + });*/ this.fileInput.addEventListener("change", () => { this.uploadFiles(); }); diff --git a/src/snek/view/channel.py b/src/snek/view/channel.py index 0ed975d..604725a 100644 --- a/src/snek/view/channel.py +++ b/src/snek/view/channel.py @@ -192,13 +192,18 @@ class ChannelAttachmentUploadView(BaseView): channel_uid=channel_uid, name=filename, user_uid=user_uid ) pathlib.Path(attachment["path"]).parent.mkdir(parents=True, exist_ok=True) - async with aiofiles.open(attachment["path"], "wb") as f: + + async with aiofiles.open(attachment["path"], "wb") as file: + print("File openend.", filename) async for msg in ws: if msg.type == web.WSMsgType.BINARY: + print("Binary",filename) if file is not None: await file.write(msg.data) - await ws.send_json({"type": "progress", "filename": filename, "bytes": file.tell()}) + await ws.send_json({"type": "progress", "filename": filename, "bytes": await file.tell()}) elif msg.type == web.WSMsgType.TEXT: + print("TExt",filename) + print(msg.json()) data = msg.json() if data.get('type') == 'end': await ws.send_json({"type": "done", "filename": filename}) From 75f12c1971e23ea7b3f63a88eeb2426bcfb70e03 Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 14 Jun 2025 08:24:48 +0200 Subject: [PATCH 02/18] We did do the cool stuff. --- src/snek/model/channel.py | 6 +++++- src/snek/service/channel_message.py | 16 ++++++++++----- src/snek/service/user.py | 1 + src/snek/static/file-upload-grid.js | 2 +- src/snek/system/mapper.py | 31 +++++++++++++++++++++-------- src/snek/templates/app.html | 1 + src/snek/view/rpc.py | 9 ++++++++- src/snek/view/web.py | 3 ++- 8 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/snek/model/channel.py b/src/snek/model/channel.py index 0a90c39..6e32335 100644 --- a/src/snek/model/channel.py +++ b/src/snek/model/channel.py @@ -11,11 +11,15 @@ class ChannelModel(BaseModel): is_listed = ModelField(name="is_listed", required=True, kind=bool, value=True) index = ModelField(name="index", required=True, kind=int, value=1000) last_message_on = ModelField(name="last_message_on", required=False, kind=str) + history_start = ModelField(name="history_start", required=False, kind=str) async def get_last_message(self) -> ChannelMessageModel: + history_start_filter = "" + if self["history_start"]: + history_start_filter = f" AND created_at > '{self['history_start']}' " try: async for model in self.app.services.channel_message.query( - "SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1", + "SELECT uid FROM channel_message WHERE channel_uid=:channel_uid" + history_start_filter + " ORDER BY created_at DESC LIMIT 1", {"channel_uid": self["uid"]}, ): diff --git a/src/snek/service/channel_message.py b/src/snek/service/channel_message.py index bfc5954..c27571b 100644 --- a/src/snek/service/channel_message.py +++ b/src/snek/service/channel_message.py @@ -88,12 +88,18 @@ class ChannelMessageService(BaseService): return await super().save(model) async def offset(self, channel_uid, page=0, timestamp=None, page_size=30): + channel = await self.services.channel.get(uid=channel_uid) + if not channel: + return [] + history_start_filter = "" + if channel["history_start"]: + history_start_filter = f" AND created_at > '{channel['history_start']}'" results = [] offset = page * page_size try: if timestamp: async for model in self.query( - "SELECT * FROM channel_message WHERE channel_uid=:channel_uid AND created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size OFFSET :offset", + f"SELECT * FROM channel_message WHERE channel_uid=:channel_uid AND created_at < :timestamp {history_start_filter} ORDER BY created_at DESC LIMIT :page_size OFFSET :offset", { "channel_uid": channel_uid, "page_size": page_size, @@ -104,7 +110,7 @@ class ChannelMessageService(BaseService): results.append(model) elif page > 0: async for model in self.query( - "SELECT * FROM channel_message WHERE channel_uid=:channel_uid WHERE created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size", + f"SELECT * FROM channel_message WHERE channel_uid=:channel_uid WHERE created_at < :timestamp {history_start_filter} ORDER BY created_at DESC LIMIT :page_size", { "channel_uid": channel_uid, "page_size": page_size, @@ -115,7 +121,7 @@ class ChannelMessageService(BaseService): results.append(model) else: async for model in self.query( - "SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT :page_size OFFSET :offset", + f"SELECT * FROM channel_message WHERE channel_uid=:channel_uid {history_start_filter} ORDER BY created_at DESC LIMIT :page_size OFFSET :offset", { "channel_uid": channel_uid, "page_size": page_size, @@ -124,7 +130,7 @@ class ChannelMessageService(BaseService): ): results.append(model) - except: - pass + except Exception as ex: + print(ex) results.sort(key=lambda x: x["created_at"]) return results diff --git a/src/snek/service/user.py b/src/snek/service/user.py index 34dc468..09b2a09 100644 --- a/src/snek/service/user.py +++ b/src/snek/service/user.py @@ -12,6 +12,7 @@ class UserService(BaseService): async def search(self, query, **kwargs): query = query.strip().lower() + kwarggs["deleted_at"] = None if not query: return [] results = [] diff --git a/src/snek/static/file-upload-grid.js b/src/snek/static/file-upload-grid.js index 0a623f6..32669a7 100644 --- a/src/snek/static/file-upload-grid.js +++ b/src/snek/static/file-upload-grid.js @@ -149,7 +149,7 @@ class FileUploadGrid extends NjetComponent { }; ws.onmessage = (event) => { - cconsole.info(event.data) + console.info(event.data) const data = JSON.parse(event.data); if (data.type === 'progress') { diff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py index fef7784..aa4af18 100644 --- a/src/snek/system/mapper.py +++ b/src/snek/system/mapper.py @@ -25,9 +25,11 @@ class BaseMapper: return asyncio.get_event_loop() async def run_in_executor(self, func, *args, **kwargs): - async with self.semaphore: - return func(*args, **kwargs) - # return await self.loop.run_in_executor(None, lambda: func(*args, **kwargs)) + use_semaphore = kwargs.pop("use_semaphore", False) + if use_semaphore: + async with self.semaphore: + return func(*args, **kwargs) + return await self.loop.run_in_executor(None, lambda: func(*args, **kwargs)) async def new(self): return self.model_class(mapper=self, app=self.app) @@ -39,7 +41,8 @@ class BaseMapper: async def get(self, uid: str = None, **kwargs) -> BaseModel: if uid: kwargs["uid"] = uid - + if not kwargs.get("deleted_at"): + kwargs["deleted_at"] = None record = await self.run_in_executor(self.table.find_one, **kwargs) if not record: return None @@ -48,7 +51,6 @@ class BaseMapper: for key, value in record.items(): model[key] = value return model - return await self.model_class.from_record(mapper=self, record=record) async def exists(self, **kwargs): return await self.run_in_executor(self.table.exists, **kwargs) @@ -60,26 +62,39 @@ class BaseMapper: if not model.record.get("uid"): raise Exception(f"Attempt to save without uid: {model.record}.") model.updated_at.update() - return await self.run_in_executor(self.table.upsert, model.record, ["uid"]) + return await self.run_in_executor(self.table.upsert, model.record, ["uid"],use_semaphore=True) async def find(self, **kwargs) -> typing.AsyncGenerator: if not kwargs.get("_limit"): kwargs["_limit"] = self.default_limit + if not kwargs.get("deleted_at"): + kwargs["deleted_at"] = None for record in await self.run_in_executor(self.table.find, **kwargs): model = await self.new() for key, value in record.items(): model[key] = value yield model + async def _use_semaphore(self, sql): + sql = sql.lower().strip() + return "insert" in sql or "update" in sql or "delete" in sql + async def query(self, sql, *args): - for record in await self.run_in_executor(self.db.query, sql, *args): + for record in await self.run_in_executor(self.db.query, sql, *args, use_semaphore=await self._use_semaphore(sql)): yield dict(record) async def update(self, model): + if not model["deleted_at"] is None: + raise Exception("Can't update deleted record.") model.updated_at.update() - return await self.run_in_executor(self.table.update, model.record, ["uid"]) + return await self.run_in_executor(self.table.update, model.record, ["uid"],use_semaphore=True) + + async def upsert(self, model): + model.updated_at.update() + return await self.run_in_executor(self.table.upsert, model.record, ["uid"],use_semaphore=True) async def delete(self, **kwargs) -> int: if not kwargs or not isinstance(kwargs, dict): raise Exception("Can't execute delete with no filter.") + kwargs["use_semaphore"] = True return await self.run_in_executor(self.table.delete, **kwargs) diff --git a/src/snek/templates/app.html b/src/snek/templates/app.html index be50cdf..eff9c2a 100644 --- a/src/snek/templates/app.html +++ b/src/snek/templates/app.html @@ -18,6 +18,7 @@ + diff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py index e7e9468..29a4f65 100644 --- a/src/snek/view/rpc.py +++ b/src/snek/view/rpc.py @@ -202,7 +202,14 @@ class RPCView(BaseView): } ) return channels - + + async def clear_channel(self, channel_uid): + self._require_login() + user = await self.services.user.get(uid=self.user_uid) + if not user["is_admin"]: + raise Exception("Not allowed") + return await self.services.channel_message.clear(channel_uid) + async def write_container(self, channel_uid, content,timeout=3): self._require_login() channel_member = await self.services.channel_member.get( diff --git a/src/snek/view/web.py b/src/snek/view/web.py index 700aa23..6e27d24 100644 --- a/src/snek/view/web.py +++ b/src/snek/view/web.py @@ -71,6 +71,7 @@ class WebView(BaseView): await self.app.services.channel_member.save(channel_member) user = await self.services.user.get(uid=self.session.get("uid")) + messages = [ await self.app.services.channel_message.to_extended_dict(message) for message in await self.app.services.channel_message.offset( @@ -81,7 +82,7 @@ class WebView(BaseView): await self.app.services.notification.mark_as_read( self.session.get("uid"), message["uid"] ) - + print(messages) name = await channel_member.get_name() return await self.render_template( "web.html", From 52538d018128915d12a3b13c21071d3fc3a41a97 Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 14 Jun 2025 08:31:37 +0200 Subject: [PATCH 03/18] Update. --- src/snek/static/editor.js | 226 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 src/snek/static/editor.js diff --git a/src/snek/static/editor.js b/src/snek/static/editor.js new file mode 100644 index 0000000..87f2da8 --- /dev/null +++ b/src/snek/static/editor.js @@ -0,0 +1,226 @@ +import { NjetComponent} from "/njext.ks" + + class NjetEditor extends NjetComponent { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + + const style = document.createElement('style'); + style.textContent = ` + #editor { + padding: 1rem; + outline: none; + white-space: pre-wrap; + line-height: 1.5; + height: 100%; + overflow-y: auto; + background: #1e1e1e; + color: #d4d4d4; + } + #command-line { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + padding: 0.2rem 1rem; + background: #333; + color: #0f0; + display: none; + font-family: monospace; + } + `; + + this.editor = document.createElement('div'); + this.editor.id = 'editor'; + this.editor.contentEditable = true; + this.editor.innerText = `Welcome to VimEditor Component +Line 2 here +Another line +Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`; + + this.cmdLine = document.createElement('div'); + this.cmdLine.id = 'command-line'; + this.shadowRoot.append(style, this.editor, this.cmdLine); + + this.mode = 'normal'; // normal | insert | visual | command + this.keyBuffer = ''; + this.lastDeletedLine = ''; + this.yankedLine = ''; + + this.editor.addEventListener('keydown', this.handleKeydown.bind(this)); + } + + connectedCallback() { + this.editor.focus(); + } + + getCaretOffset() { + let caretOffset = 0; + const sel = this.shadowRoot.getSelection(); + if (!sel || sel.rangeCount === 0) return 0; + + const range = sel.getRangeAt(0); + const preCaretRange = range.cloneRange(); + preCaretRange.selectNodeContents(this.editor); + preCaretRange.setEnd(range.endContainer, range.endOffset); + caretOffset = preCaretRange.toString().length; + return caretOffset; + } + + setCaretOffset(offset) { + const range = document.createRange(); + const sel = this.shadowRoot.getSelection(); + const walker = document.createTreeWalker(this.editor, NodeFilter.SHOW_TEXT, null, false); + + let currentOffset = 0; + let node; + while ((node = walker.nextNode())) { + if (currentOffset + node.length >= offset) { + range.setStart(node, offset - currentOffset); + range.collapse(true); + sel.removeAllRanges(); + sel.addRange(range); + return; + } + currentOffset += node.length; + } + } + + handleKeydown(e) { + const key = e.key; + + if (this.mode === 'insert') { + if (key === 'Escape') { + e.preventDefault(); + this.mode = 'normal'; + this.editor.blur(); + this.editor.focus(); + } + return; + } + + if (this.mode === 'command') { + if (key === 'Enter' || key === 'Escape') { + e.preventDefault(); + this.cmdLine.style.display = 'none'; + this.mode = 'normal'; + this.keyBuffer = ''; + } + return; + } + + if (this.mode === 'visual') { + if (key === 'Escape') { + e.preventDefault(); + this.mode = 'normal'; + } + return; + } + + // Handle normal mode + this.keyBuffer += key; + + const text = this.editor.innerText; + const caretPos = this.getCaretOffset(); + const lines = text.split('\n'); + + let charCount = 0, lineIdx = 0; + for (let i = 0; i < lines.length; i++) { + if (caretPos <= charCount + lines[i].length) { + lineIdx = i; + break; + } + charCount += lines[i].length + 1; + } + + const offsetToLine = idx => + text.split('\n').slice(0, idx).reduce((acc, l) => acc + l.length + 1, 0); + + switch (this.keyBuffer) { + case 'i': + e.preventDefault(); + this.mode = 'insert'; + this.keyBuffer = ''; + break; + + case 'v': + e.preventDefault(); + this.mode = 'visual'; + this.keyBuffer = ''; + break; + + case ':': + e.preventDefault(); + this.mode = 'command'; + this.cmdLine.style.display = 'block'; + this.cmdLine.textContent = ':'; + this.keyBuffer = ''; + break; + + case 'yy': + e.preventDefault(); + this.yankedLine = lines[lineIdx]; + this.keyBuffer = ''; + break; + + case 'dd': + e.preventDefault(); + this.lastDeletedLine = lines[lineIdx]; + lines.splice(lineIdx, 1); + this.editor.innerText = lines.join('\n'); + this.setCaretOffset(offsetToLine(lineIdx)); + this.keyBuffer = ''; + break; + + case 'p': + e.preventDefault(); + const lineToPaste = this.yankedLine || this.lastDeletedLine; + if (lineToPaste) { + lines.splice(lineIdx + 1, 0, lineToPaste); + this.editor.innerText = lines.join('\n'); + this.setCaretOffset(offsetToLine(lineIdx + 1)); + } + this.keyBuffer = ''; + break; + + case '0': + e.preventDefault(); + this.setCaretOffset(offsetToLine(lineIdx)); + this.keyBuffer = ''; + break; + + case '$': + e.preventDefault(); + this.setCaretOffset(offsetToLine(lineIdx) + lines[lineIdx].length); + this.keyBuffer = ''; + break; + + case 'gg': + e.preventDefault(); + this.setCaretOffset(0); + this.keyBuffer = ''; + break; + + case 'G': + e.preventDefault(); + this.setCaretOffset(text.length); + this.keyBuffer = ''; + break; + + case 'Escape': + e.preventDefault(); + this.mode = 'normal'; + this.keyBuffer = ''; + this.cmdLine.style.display = 'none'; + break; + + default: + // allow up to 2 chars for combos + if (this.keyBuffer.length > 2) this.keyBuffer = ''; + break; + } + } + } + + customElements.define('njet-editor', NjetEditor); +export {NjetEditor} From a4d29b9d6fb59dffa2021c4cfc1ab65b25015cee Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 14 Jun 2025 09:15:08 +0200 Subject: [PATCH 04/18] Update. --- src/snek/app.py | 25 +++++++++++++++- src/snek/system/cache.py | 2 +- src/snek/system/mapper.py | 8 +++++- src/snek/system/service.py | 10 +++---- src/snek/view/rpc.py | 58 +++++++++++++++++++------------------- 5 files changed, 66 insertions(+), 37 deletions(-) diff --git a/src/snek/app.py b/src/snek/app.py index 9822d2d..09df0bd 100644 --- a/src/snek/app.py +++ b/src/snek/app.py @@ -5,6 +5,7 @@ import ssl import uuid import signal from datetime import datetime +from contextlib import asynccontextmanager from snek import snode from snek.view.threads import ThreadsView @@ -230,6 +231,7 @@ class Application(BaseApplication): except Exception as ex: print(ex) self.db.commit() + async def prepare_database(self, app): self.db.query("PRAGMA journal_mode=WAL") @@ -452,7 +454,28 @@ class Application(BaseApplication): template_paths.append(self.template_path) return FileSystemLoader(template_paths) - + + @asynccontextmanager + async def no_save(self): + stats = { + 'count': 0 + } + async def patched_save(*args, **kwargs): + await self.cache.set(args[0]["uid"], args[0]) + stats['count'] = stats['count'] + 1 + print(f"save is ignored {stats['count']} times") + return args[0] + save_original = app.services.channel_message.mapper.save + self.services.channel_message.mapper.save = patched_save + raised_exception = None + try: + yield + except Exception as ex: + raised_exception = ex + finally: + self.services.channel_message.mapper.save = save_original + if raised_exception: + raise raised_exception app = Application(db_path="sqlite:///snek.db") diff --git a/src/snek/system/cache.py b/src/snek/system/cache.py index 8f8cdc3..19b8ebf 100644 --- a/src/snek/system/cache.py +++ b/src/snek/system/cache.py @@ -14,7 +14,7 @@ class Cache: self.cache = {} self.max_items = max_items self.stats = {} - self.enabled = False + self.enabled = True self.lru = [] self.version = ((42 + 420 + 1984 + 1990 + 10 + 6 + 71 + 3004 + 7245) ^ 1337) + 4 diff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py index aa4af18..22f958e 100644 --- a/src/snek/system/mapper.py +++ b/src/snek/system/mapper.py @@ -28,7 +28,13 @@ class BaseMapper: use_semaphore = kwargs.pop("use_semaphore", False) if use_semaphore: async with self.semaphore: - return func(*args, **kwargs) + database_exception = None + for x in range(20): + try: + return func(*args, **kwargs) + except Exception as ex: + database_exception = ex + raise database_exception return await self.loop.run_in_executor(None, lambda: func(*args, **kwargs)) async def new(self): diff --git a/src/snek/system/service.py b/src/snek/system/service.py index bf0c9d6..41c87a4 100644 --- a/src/snek/system/service.py +++ b/src/snek/system/service.py @@ -40,11 +40,11 @@ class BaseService: yield record async def get(self, uid=None, **kwargs): + kwargs["deleted_at"] = None if uid: - if not kwargs: - result = await self.cache.get(uid) - if False and result and result.__class__ == self.mapper.model_class: - return result + result = await self.cache.get(uid) + if result and result.__class__ == self.mapper.model_class: + return result kwargs["uid"] = uid result = await self.mapper.get(**kwargs) @@ -52,7 +52,7 @@ class BaseService: await self.cache.set(result["uid"], result) return result - async def save(self, model: UserModel): + async def save(self, model): # if model.is_valid: You Know why not if await self.mapper.save(model): await self.cache.set(model["uid"], model) diff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py index 29a4f65..5edc494 100644 --- a/src/snek/view/rpc.py +++ b/src/snek/view/rpc.py @@ -308,40 +308,40 @@ class RPCView(BaseView): return True async def update_message_text(self, message_uid, text): - self._require_login() - message = await self.services.channel_message.get(message_uid) - if message["user_uid"] != self.user_uid: - raise Exception("Not allowed") + async with self.app.no_save(): + self._require_login() + message = await self.services.channel_message.get(message_uid) + if message["user_uid"] != self.user_uid: + raise Exception("Not allowed") - if message.get_seconds_since_last_update() > 3: - await self.finalize_message(message["uid"]) - return { - "error": "Message too old", - "seconds_since_last_update": message.get_seconds_since_last_update(), - "success": False, - } + if message.get_seconds_since_last_update() > 5: + return { + "error": "Message too old", + "seconds_since_last_update": message.get_seconds_since_last_update(), + "success": False, + } - message["message"] = text - if not text: - message["deleted_at"] = now() - else: - message["deleted_at"] = None + message["message"] = text + if not text: + message["deleted_at"] = now() + else: + message["deleted_at"] = None - await self.services.channel_message.save(message) - data = message.record - data["text"] = message["message"] - data["message_uid"] = message_uid + await self.services.channel_message.save(message) + data = message.record + data["text"] = message["message"] + data["message_uid"] = message_uid - await self.services.socket.broadcast( - message["channel_uid"], - { - "channel_uid": message["channel_uid"], - "event": "update_message_text", - "data": message.record, - }, - ) + await self.services.socket.broadcast( + message["channel_uid"], + { + "channel_uid": message["channel_uid"], + "event": "update_message_text", + "data": message.record, + }, + ) - return {"success": True} + return {"success": True} async def send_message(self, channel_uid, message, is_final=True): self._require_login() From 2c182ad48d1d2821d9665f5f7d82c06d55a48d7d Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 14 Jun 2025 12:34:26 +0200 Subject: [PATCH 05/18] Update. --- src/snek/app.py | 5 +++++ src/snek/service/channel.py | 6 ++++++ src/snek/static/editor.js | 2 +- src/snek/system/mapper.py | 1 + src/snek/system/middleware.py | 35 ++++++++++++++++------------------- src/snek/templates/app.html | 2 +- src/snek/view/rpc.py | 12 ++++++++++++ 7 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/snek/app.py b/src/snek/app.py index 09df0bd..177c38e 100644 --- a/src/snek/app.py +++ b/src/snek/app.py @@ -408,6 +408,11 @@ class Application(BaseApplication): self.jinja2_env.loader = await self.get_user_template_loader( request.session.get("uid") ) + + try: + context["nonce"] = request['csp_nonce'] + except: + context['nonce'] = '?' rendered = await super().render_template(template, request, context) diff --git a/src/snek/service/channel.py b/src/snek/service/channel.py index 0af0a4b..5d15683 100644 --- a/src/snek/service/channel.py +++ b/src/snek/service/channel.py @@ -113,6 +113,12 @@ class ChannelService(BaseService): channel = await self.get(uid=channel_member["channel_uid"]) yield channel + async def clear(self, channel_uid): + model = await self.get(uid=channel_uid) + model['history_from'] = datetime.now() + await self.save(model) + + async def ensure_public_channel(self, created_by_uid): model = await self.get(is_listed=True, tag="public") is_moderator = False diff --git a/src/snek/static/editor.js b/src/snek/static/editor.js index 87f2da8..8ee3bd3 100644 --- a/src/snek/static/editor.js +++ b/src/snek/static/editor.js @@ -1,4 +1,4 @@ -import { NjetComponent} from "/njext.ks" +import { NjetComponent} from "/njet.js" class NjetEditor extends NjetComponent { constructor() { diff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py index 22f958e..d7f1451 100644 --- a/src/snek/system/mapper.py +++ b/src/snek/system/mapper.py @@ -33,6 +33,7 @@ class BaseMapper: try: return func(*args, **kwargs) except Exception as ex: + await asyncio.sleep(0) database_exception = ex raise database_exception return await self.loop.run_in_executor(None, lambda: func(*args, **kwargs)) diff --git a/src/snek/system/middleware.py b/src/snek/system/middleware.py index 93e5eaf..a84e5cf 100644 --- a/src/snek/system/middleware.py +++ b/src/snek/system/middleware.py @@ -10,28 +10,25 @@ import secrets from aiohttp import web -csp_policy = ( - "default-src 'self'; " - "script-src 'self' https://*.cloudflare.com https://molodetz.nl 'nonce-{nonce}'; " - "style-src 'self' https://*.cloudflare.com https://molodetz.nl; " - "img-src 'self' https://*.cloudflare.com https://molodetz.nl data:; " - "connect-src 'self' https://*.cloudflare.com https://molodetz.nl;" -) - - -def generate_nonce(): - return secrets.token_hex(16) - - @web.middleware async def csp_middleware(request, handler): - + nonce = str(secrets.token_hex(16)) + print("Nonce:", nonce) + csp_policy = ( + "default-src 'self'; " + f"script-src 'self' https://umami.molodetz.nl 'nonce-{nonce}'; " + "style-src 'self'; " + "img-src *; " + "connect-src 'self'; https://umami.molodetz.nl; 'nonce-{nonce}';" + "font-src 'self'; " + "object-src 'none'; " + "base-uri 'self'; " + "form-action 'self';" + ) + request['csp_nonce'] = nonce response = await handler(request) - return response - nonce = generate_nonce() - response.headers["Content-Security-Policy"] = csp_policy.format(nonce=nonce) - return response - + response.headers['Content-Security-Policy'] = csp_policy + return response @web.middleware async def no_cors_middleware(request, handler): diff --git a/src/snek/templates/app.html b/src/snek/templates/app.html index eff9c2a..f5b0481 100644 --- a/src/snek/templates/app.html +++ b/src/snek/templates/app.html @@ -31,7 +31,7 @@ - +
diff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py index 5edc494..7345d53 100644 --- a/src/snek/view/rpc.py +++ b/src/snek/view/rpc.py @@ -343,6 +343,18 @@ class RPCView(BaseView): return {"success": True} + async def clear_channel(self, channel_uid): + self._require_login() + user = await self.services.user.get(uid=self.user_uid) + if not user["is_admin"]: + raise Exception("Not allowed") + channel = await self.services.channel.get(uid=channel_uid) + if not channel: + raise Exception("Channel not found") + channel['history_start'] = datetime.now() + await self.services.channel.save(channel) + return await self.services.channel_message.clear(channel_uid) + async def send_message(self, channel_uid, message, is_final=True): self._require_login() message = await self.services.chat.send( From bf576bc0e3ab594ed01f4d7eed2247efb799b836 Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 14 Jun 2025 12:42:34 +0200 Subject: [PATCH 06/18] Update. --- src/snek/system/middleware.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/snek/system/middleware.py b/src/snek/system/middleware.py index a84e5cf..d2e4edd 100644 --- a/src/snek/system/middleware.py +++ b/src/snek/system/middleware.py @@ -1,13 +1,8 @@ # Written by retoor@molodetz.nl -# This code provides middleware functions for an aiohttp server to manage and modify CORS (Cross-Origin Resource Sharing) headers. - -# Imports from 'aiohttp' library are used to create middleware; they are not part of Python's standard library. - -# MIT License: This code is distributed under the MIT License. +# This code provides middleware functions for an aiohttp server to manage and modify CSP, CORS, and authentication headers. import secrets - from aiohttp import web @web.middleware @@ -17,18 +12,22 @@ async def csp_middleware(request, handler): csp_policy = ( "default-src 'self'; " f"script-src 'self' https://umami.molodetz.nl 'nonce-{nonce}'; " - "style-src 'self'; " - "img-src *; " - "connect-src 'self'; https://umami.molodetz.nl; 'nonce-{nonce}';" - "font-src 'self'; " + "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " + "img-src 'self' data: https://umodetz.nl; " + "connect-src 'self' https://umodetz.nl; " + "font-src 'self' data:; " "object-src 'none'; " "base-uri 'self'; " - "form-action 'self';" + "form-action 'self'; " + "frame-src 'self'; " + "worker-src 'self'; " + "media-src 'self'; " + "manifest-src 'self';" ) request['csp_nonce'] = nonce response = await handler(request) response.headers['Content-Security-Policy'] = csp_policy - return response + return response @web.middleware async def no_cors_middleware(request, handler): @@ -36,7 +35,6 @@ async def no_cors_middleware(request, handler): response.headers.pop("Access-Control-Allow-Origin", None) return response - @web.middleware async def cors_allow_middleware(request, handler): response = await handler(request) @@ -48,7 +46,6 @@ async def cors_allow_middleware(request, handler): response.headers["Access-Control-Allow-Credentials"] = "true" return response - @web.middleware async def auth_middleware(request, handler): request["user"] = None @@ -58,7 +55,6 @@ async def auth_middleware(request, handler): ) return await handler(request) - @web.middleware async def cors_middleware(request, handler): if request.headers.get("Allow"): From 6a905c1948432818fe9b8c601fc20269cb08901e Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 14 Jun 2025 12:53:40 +0200 Subject: [PATCH 07/18] Nice. --- src/snek/system/middleware.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/snek/system/middleware.py b/src/snek/system/middleware.py index d2e4edd..cb2f6bd 100644 --- a/src/snek/system/middleware.py +++ b/src/snek/system/middleware.py @@ -7,21 +7,20 @@ from aiohttp import web @web.middleware async def csp_middleware(request, handler): - nonce = str(secrets.token_hex(16)) - print("Nonce:", nonce) + nonce = secrets.token_hex(16) csp_policy = ( "default-src 'self'; " f"script-src 'self' https://umami.molodetz.nl 'nonce-{nonce}'; " "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " - "img-src 'self' data: https://umodetz.nl; " - "connect-src 'self' https://umodetz.nl; " - "font-src 'self' data:; " + "img-src *; " + "connect-src 'self' https://umami.molodetz.nl; " + "font-src *; " "object-src 'none'; " "base-uri 'self'; " "form-action 'self'; " "frame-src 'self'; " - "worker-src 'self'; " - "media-src 'self'; " + "worker-src *; " + "media-src *; " "manifest-src 'self';" ) request['csp_nonce'] = nonce From 3872dafaf1eff40ec8c2ef63017a1cddb97a1425 Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 14 Jun 2025 13:02:53 +0200 Subject: [PATCH 08/18] Nice. --- src/snek/system/middleware.py | 7 +++++-- src/snek/templates/sandbox.html | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/snek/system/middleware.py b/src/snek/system/middleware.py index cb2f6bd..8e8a906 100644 --- a/src/snek/system/middleware.py +++ b/src/snek/system/middleware.py @@ -5,13 +5,15 @@ import secrets from aiohttp import web + @web.middleware async def csp_middleware(request, handler): nonce = secrets.token_hex(16) + origin = request.headers.get('Origin') csp_policy = ( "default-src 'self'; " - f"script-src 'self' https://umami.molodetz.nl 'nonce-{nonce}'; " - "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " + f"script-src 'self' {origin} 'nonce-{nonce}'; " + f"style-src 'self' 'unsafe-inline' {origin} 'nonce-{nonce}'; " "img-src *; " "connect-src 'self' https://umami.molodetz.nl; " "font-src *; " @@ -28,6 +30,7 @@ async def csp_middleware(request, handler): response.headers['Content-Security-Policy'] = csp_policy return response + @web.middleware async def no_cors_middleware(request, handler): response = await handler(request) diff --git a/src/snek/templates/sandbox.html b/src/snek/templates/sandbox.html index 4949247..7e2a038 100644 --- a/src/snek/templates/sandbox.html +++ b/src/snek/templates/sandbox.html @@ -1,6 +1,6 @@
-