From 8bacd6aa3f25331f21409218a894e2a6cbf504f3 Mon Sep 17 00:00:00 2001 From: retoor Date: Thu, 18 Dec 2025 23:48:50 +0100 Subject: [PATCH] feat: add debug logging option to serve command refactor: reorganize imports and improve error handling in application fix: correct typo in ip2location middleware chore: add author headers to source files --- CHANGELOG.md | 8 ++ pyproject.toml | 2 +- src/snek/__init__.py | 2 + src/snek/__main__.py | 27 +++-- src/snek/app.py | 112 +++++++----------- src/snek/balancer.py | 2 + src/snek/docs/app.py | 2 + src/snek/dump.py | 2 + src/snek/form/__init__.py | 3 + src/snek/form/login.py | 2 + src/snek/form/register.py | 2 + src/snek/form/search_user.py | 2 + src/snek/form/settings/profile.py | 2 + src/snek/forum.py | 2 + src/snek/gunicorn.py | 2 + src/snek/mapper/__init__.py | 2 + src/snek/mapper/channel.py | 2 + src/snek/mapper/channel_attachment.py | 2 + src/snek/mapper/channel_member.py | 2 + src/snek/mapper/channel_message.py | 2 + src/snek/mapper/container.py | 2 + src/snek/mapper/drive.py | 2 + src/snek/mapper/drive_item.py | 2 + src/snek/mapper/forum.py | 2 + src/snek/mapper/notification.py | 2 + src/snek/mapper/profile_page.py | 2 + src/snek/mapper/push.py | 2 + src/snek/mapper/repository.py | 2 + src/snek/mapper/user.py | 8 +- src/snek/mapper/user_property.py | 2 + src/snek/model/__init__.py | 4 +- src/snek/model/channel.py | 5 +- src/snek/model/channel_attachment.py | 2 + src/snek/model/channel_member.py | 2 + src/snek/model/channel_message.py | 2 + src/snek/model/container.py | 2 + src/snek/model/drive.py | 2 + src/snek/model/drive_item.py | 2 + src/snek/model/forum.py | 2 + src/snek/model/notification.py | 2 + src/snek/model/profile_page.py | 2 + src/snek/model/push_registration.py | 2 + src/snek/model/repository.py | 2 + src/snek/model/user.py | 2 + src/snek/model/user_property.py | 2 + src/snek/research/serpentarium.py | 44 +++++-- src/snek/research/serptest.py | 2 + src/snek/service/__init__.py | 4 +- src/snek/service/channel.py | 4 +- src/snek/service/channel_attachment.py | 2 + src/snek/service/channel_member.py | 7 +- src/snek/service/channel_message.py | 80 ++++++------- src/snek/service/chat.py | 21 ++++ src/snek/service/container.py | 3 +- src/snek/service/db.py | 2 + src/snek/service/drive.py | 2 + src/snek/service/drive_item.py | 2 + src/snek/service/forum.py | 2 + src/snek/service/notification.py | 8 +- src/snek/service/profile_page.py | 2 + src/snek/service/push.py | 2 + src/snek/service/repository.py | 2 + src/snek/service/socket.py | 16 ++- src/snek/service/statistics.py | 8 +- src/snek/service/user.py | 6 +- src/snek/service/user_property.py | 2 + src/snek/service/util.py | 2 + src/snek/sgit.py | 2 + src/snek/shell.py | 2 + src/snek/snode.py | 2 + src/snek/sssh.py | 2 + src/snek/static/app.js | 24 +--- src/snek/static/chat-input.js | 31 ++++- src/snek/static/chat-window.js | 2 + src/snek/static/container.js | 5 +- src/snek/static/dumb-term.js | 3 +- src/snek/static/editor.js | 14 ++- src/snek/static/fancy-button.js | 2 + src/snek/static/file-manager.js | 2 + src/snek/static/file-upload-grid.js | 2 + src/snek/static/generic-form.js | 2 + src/snek/static/html-frame.js | 2 + src/snek/static/logger.js | 98 +++++++++++++++ src/snek/static/markdown-frame.js | 2 + src/snek/static/media-upload.js | 2 + src/snek/static/message-list-manager.js | 2 + src/snek/static/message-list.js | 18 +-- src/snek/static/models.js | 2 + src/snek/static/njet.js | 2 + src/snek/static/online-users.js | 3 + .../static/polyfills/Promise.withResolvers.js | 4 +- src/snek/static/push.js | 6 +- src/snek/static/schedule.js | 2 + src/snek/static/service-worker.js | 2 + src/snek/static/socket.js | 48 ++++++-- src/snek/static/stt.js | 12 +- src/snek/static/tts.js | 3 +- src/snek/static/upload-button.js | 2 + src/snek/static/user-list.js | 2 + src/snek/sync.py | 31 +++-- src/snek/system/__init__.py | 3 + src/snek/system/api.py | 3 + src/snek/system/cache.py | 6 +- src/snek/system/debug.py | 2 + src/snek/system/docker.py | 3 +- src/snek/system/exceptions.py | 2 + src/snek/system/form.py | 2 + src/snek/system/http.py | 30 +---- src/snek/system/mapper.py | 26 ++-- src/snek/system/markdown.py | 5 +- src/snek/system/middleware.py | 2 + src/snek/system/model.py | 14 +-- src/snek/system/object.py | 2 + src/snek/system/profiler.py | 2 + src/snek/system/security.py | 2 + src/snek/system/service.py | 6 +- src/snek/system/template.py | 4 +- src/snek/system/view.py | 5 +- src/snek/view/__init__.py | 3 + src/snek/view/about.py | 2 + src/snek/view/avatar.py | 2 + src/snek/view/avatar_animal_view.py | 2 + src/snek/view/channel.py | 2 + src/snek/view/docs.py | 2 + src/snek/view/drive.py | 2 + src/snek/view/forum.py | 2 + src/snek/view/git_docs.py | 2 + src/snek/view/index.py | 2 + src/snek/view/login.py | 2 + src/snek/view/login_form.py | 2 + src/snek/view/logout.py | 2 + src/snek/view/new.py | 4 +- src/snek/view/profile_page.py | 2 + src/snek/view/push.py | 2 + src/snek/view/register.py | 2 + src/snek/view/register_form.py | 2 + src/snek/view/rpc.py | 71 ++++++++--- src/snek/view/search_user.py | 2 + src/snek/view/settings/containers.py | 2 + src/snek/view/settings/index.py | 2 + src/snek/view/settings/profile.py | 2 + src/snek/view/settings/profile_pages.py | 2 + src/snek/view/settings/repositories.py | 2 + src/snek/view/stats.py | 2 + src/snek/view/status.py | 2 + src/snek/view/terminal.py | 2 + src/snek/view/threads.py | 2 + src/snek/view/user.py | 2 + src/snek/view/web.py | 2 + src/snek/webdav.py | 2 + 150 files changed, 751 insertions(+), 308 deletions(-) create mode 100644 src/snek/static/logger.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fa2872..fd0b89d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ + +## Version 1.9.0 - 2025-12-18 + +Adds a debug logging option to the serve command for enhanced troubleshooting. Improves error handling across the application and corrects a typo in the ip2location middleware. + +**Changes:** 148 files, 1047 lines +**Languages:** JavaScript (299 lines), Python (748 lines) + ## Version 1.8.0 - 2025-12-18 The socket service now handles errors more robustly and prevents crashes through improved safety checks. Socket methods support better concurrency and provide enhanced logging for developers. diff --git a/pyproject.toml b/pyproject.toml index 2db0d0b..b0451db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "Snek" -version = "1.8.0" +version = "1.9.0" readme = "README.md" #license = { file = "LICENSE", content-type="text/markdown" } description = "Snek Chat Application by Molodetz" diff --git a/src/snek/__init__.py b/src/snek/__init__.py index ba51bb5..e3a6773 100644 --- a/src/snek/__init__.py +++ b/src/snek/__init__.py @@ -1,3 +1,5 @@ +# retoor + """ MIT License diff --git a/src/snek/__main__.py b/src/snek/__main__.py index 84f448d..e90da63 100644 --- a/src/snek/__main__.py +++ b/src/snek/__main__.py @@ -1,18 +1,18 @@ -import logging - -logging.basicConfig(level=logging.INFO) +# retoor +import asyncio +import logging import pathlib import shutil import sqlite3 -import asyncio + import click from aiohttp import web -from IPython import start_ipython -from snek.shell import Shell + from snek.app import Application +from snek.shell import Shell - +logging.basicConfig(level=logging.INFO) @click.group() @@ -111,9 +111,16 @@ def init(db_path, source): show_default=True, help="Database path for the application", ) -def serve(port, host, db_path): - # init(db_path) - # asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) +@click.option( + "--debug", + is_flag=True, + default=False, + help="Enable debug logging", +) +def serve(port, host, db_path, debug): + if debug: + logging.getLogger().setLevel(logging.DEBUG) + logging.getLogger("snek").setLevel(logging.DEBUG) web.run_app(Application(db_path=f"sqlite:///{db_path}"), port=port, host=host) diff --git a/src/snek/app.py b/src/snek/app.py index 3d033c4..254adec 100644 --- a/src/snek/app.py +++ b/src/snek/app.py @@ -1,21 +1,15 @@ +# retoor + import asyncio import logging import pathlib import ssl -import uuid -import signal -from datetime import datetime -from contextlib import asynccontextmanager - -from snek import snode -from snek.view.threads import ThreadsView - -logging.basicConfig(level=logging.DEBUG) -from concurrent.futures import ThreadPoolExecutor -from ipaddress import ip_address import time import uuid - +from concurrent.futures import ThreadPoolExecutor +from contextlib import asynccontextmanager +from datetime import datetime +from ipaddress import ip_address import IP2Location from aiohttp import web @@ -28,6 +22,7 @@ from aiohttp_session.cookie_storage import EncryptedCookieStorage from app.app import Application as BaseApplication from jinja2 import FileSystemLoader +from snek import snode from snek.mapper import get_mappers from snek.service import get_services from snek.sgit import GitApplication @@ -50,6 +45,7 @@ from snek.view.channel import ChannelAttachmentView,ChannelAttachmentUploadView, from snek.view.docs import DocsHTMLView, DocsMDView from snek.view.drive import DriveApiView, DriveView from snek.view.channel import ChannelDriveApiView +from snek.view.container import ContainerView from snek.view.index import IndexView from snek.view.login import LoginView from snek.view.logout import LogoutView @@ -58,7 +54,7 @@ from snek.view.register import RegisterView from snek.view.repository import RepositoryView from snek.view.rpc import RPCView from snek.view.search_user import SearchUserView -from snek.view.container import ContainerView +from snek.view.threads import ThreadsView from snek.view.settings.containers import ( ContainersCreateView, ContainersDeleteView, @@ -88,9 +84,12 @@ from snek.view.user import UserView from snek.view.web import WebView from snek.webdav import WebdavApplication from snek.forum import setup_forum +from snek.system.template import whitelist_attributes + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34" -from snek.system.template import whitelist_attributes @web.middleware @@ -103,21 +102,22 @@ async def session_middleware(request, handler): @web.middleware async def ip2location_middleware(request, handler): response = await handler(request) - return response ip = request.headers.get("X-Forwarded-For", request.remote) - ipaddress = ip_address(ip) - if ipaddress.is_private: + try: + ipaddr = ip_address(ip) + if ipaddr.is_private: + return response + except ValueError: return response - if not request.app.session.get("uid"): + if not request.session.get("uid"): return response - user = await request.app.services.user.get(uid=request.app.session.get("uid")) + user = await request.app.services.user.get(uid=request.session.get("uid")) if not user: return response - location = request.app.ip2location.get(ip) - user["city"] + location = request.app.ip2location.get_all(ip) if user["city"] != location.city: - user["country_long"] = location.country - user["country_short"] = locaion.country_short + user["country_long"] = location.country_long + user["country_short"] = location.country_short user["city"] = location.city user["region"] = location.region user["latitude"] = location.latitude @@ -130,14 +130,12 @@ async def ip2location_middleware(request, handler): @web.middleware async def trailing_slash_middleware(request, handler): if request.path and not request.path.endswith("/"): - # Redirect to the same path with a trailing slash raise web.HTTPFound(request.path + "/") return await handler(request) class Application(BaseApplication): async def create_default_forum(self, app): - # Check if any forums exist forums = [f async for f in self.services.forum.find(is_active=True)] if not forums: # Find admin user to be the creator @@ -148,7 +146,6 @@ class Application(BaseApplication): description="A place for general discussion.", created_by_uid=admin_user["uid"], ) - print("Default forum 'General Discussion' created.") def __init__(self, *args, **kwargs): middlewares = [ @@ -160,7 +157,7 @@ class Application(BaseApplication): super().__init__( middlewares=middlewares, template_path=self.template_path, - client_max_size=1024 * 1024 * 1024 * 5 * args, + client_max_size=1024 * 1024 * 1024 * 5, **kwargs, ) session_setup(self, EncryptedCookieStorage(SESSION_KEY)) @@ -185,7 +182,7 @@ class Application(BaseApplication): self.mappers = get_mappers(app=self) self.broadcast_service = None self.user_availability_service_task = None - + self.setup_router() base_path = pathlib.Path(__file__).parent self.ip2location = IP2Location.IP2Location( @@ -196,7 +193,6 @@ class Application(BaseApplication): self.on_startup.append(self.start_ssh_server) self.on_startup.append(self.prepare_database) self.on_startup.append(self.create_default_forum) - @property def uptime_seconds(self): @@ -238,13 +234,8 @@ class Application(BaseApplication): asyncio.create_task(app.ssh_server.wait_closed()) async def prepare_asyncio(self, app): - # app.loop = asyncio.get_running_loop() app.executor = ThreadPoolExecutor(max_workers=200) app.loop.set_default_executor(self.executor) - #for sig in (signal.SIGINT, signal.SIGTERM): - #app.loop.add_signal_handler( - # sig, lambda: asyncio.create_task(self.services.container.shutdown()) - #) async def create_task(self, task): await self.tasks.put(task) @@ -257,9 +248,8 @@ class Application(BaseApplication): await task self.tasks.task_done() except Exception as ex: - print(ex) + logger.error(f"Task runner error: {ex}") self.db.commit() - async def prepare_database(self, app): self.db.query("PRAGMA journal_mode=WAL") @@ -272,8 +262,8 @@ class Application(BaseApplication): self.db["channel_member"].create_index(["channel_uid", "user_uid"]) if not self.db["channel_message"].has_index(["channel_uid", "user_uid"]): self.db["channel_message"].create_index(["channel_uid", "user_uid"]) - except: - pass + except Exception as ex: + logger.warning(f"Index creation error: {ex}") await self.services.drive.prepare_all() self.loop.create_task(self.task_runner()) @@ -306,9 +296,6 @@ class Application(BaseApplication): self.router.add_view("/login.json", LoginView) self.router.add_view("/register.html", RegisterView) self.router.add_view("/register.json", RegisterView) - # self.router.add_view("/drive/{rel_path:.*}", DriveView) - ## self.router.add_view("/drive.bin", UploadView) - # self.router.add_view("/drive.bin/{uid}.{ext}", UploadView) self.router.add_view("/search-user.html", SearchUserView) self.router.add_view("/search-user.json", SearchUserView) self.router.add_view("/avatar/{uid}.svg", AvatarView) @@ -316,18 +303,12 @@ class Application(BaseApplication): self.router.add_get("/http-photo", self.handle_http_photo) self.router.add_get("/rpc.ws", RPCView) self.router.add_get("/c/{channel:.*}", ChannelView) - #self.router.add_view( - # "/channel/{channel_uid}/attachment.bin", ChannelAttachmentView - #) - #self.router.add_view( - # "/channel/{channel_uid}/drive.json", ChannelDriveApiView - #) self.router.add_view( "/channel/{channel_uid}/attachment.sock", ChannelAttachmentUploadView ) self.router.add_view( "/channel/attachment/{relative_url:.*}", ChannelAttachmentView - )# + ) self.router.add_view("/channel/{channel}.html", WebView) self.router.add_view("/threads.html", ThreadsView) self.router.add_view("/terminal.ws", TerminalSocketView) @@ -376,10 +357,8 @@ class Application(BaseApplication): self.add_subapp("/webdav", self.webdav) self.add_subapp("/git", self.git) setup_forum(self) - # self.router.add_get("/{file_path:.*}", self.static_handler) async def handle_test(self, request): - return await whitelist_attributes( self.render_template("test.html", request, context={"name": "retoor"}) ) @@ -396,10 +375,8 @@ class Application(BaseApplication): body=path.read_bytes(), headers={"Content-Type": "image/png"} ) - # @time_cache_async(60) async def render_template(self, template, request, context=None): start_time = time.perf_counter() - channels = [] if not context: context = {} @@ -441,31 +418,25 @@ class Application(BaseApplication): request.session.get("uid") ) - self.template_path.joinpath(template) - - await self.services.user.get_template_path(request.session.get("uid")) - self.original_loader = self.jinja2_env.loader self.jinja2_env.loader = await self.get_user_template_loader( request.session.get("uid") ) - + try: - context["nonce"] = request['csp_nonce'] - except: - context['nonce'] = '?' + context["nonce"] = request["csp_nonce"] + except KeyError: + context["nonce"] = "?" rendered = await super().render_template(template, request, context) self.jinja2_env.loader = self.original_loader end_time = time.perf_counter() - print(f"render_template took {end_time - start_time:.4f} seconds") - - # rendered.text = whitelist_attributes(rendered.text) - # rendered.headers['Content-Lenght'] = len(rendered.text) + logger.debug(f"render_template took {end_time - start_time:.4f} seconds") return rendered + async def static_handler(self, request): file_name = request.match_info.get("filename", "") @@ -503,22 +474,21 @@ class Application(BaseApplication): template_paths.append(self.template_path) return FileSystemLoader(template_paths) - + @asynccontextmanager async def no_save(self): - stats = { - 'count': 0 - } + 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") + stats["count"] = stats["count"] + 1 + logger.debug(f"save is ignored {stats['count']} times") return args[0] save_original = self.services.channel_message.mapper.save self.services.channel_message.mapper.save = patched_save raised_exception = None try: - yield + yield except Exception as ex: raised_exception = ex finally: diff --git a/src/snek/balancer.py b/src/snek/balancer.py index 44db9a0..758cd60 100644 --- a/src/snek/balancer.py +++ b/src/snek/balancer.py @@ -1,3 +1,5 @@ +# retoor + import asyncio import sys diff --git a/src/snek/docs/app.py b/src/snek/docs/app.py index 50a4245..faee4d9 100644 --- a/src/snek/docs/app.py +++ b/src/snek/docs/app.py @@ -1,3 +1,5 @@ +# retoor + import pathlib from aiohttp import web diff --git a/src/snek/dump.py b/src/snek/dump.py index b254756..32527ed 100644 --- a/src/snek/dump.py +++ b/src/snek/dump.py @@ -1,3 +1,5 @@ +# retoor + import asyncio from snek.app import app diff --git a/src/snek/form/__init__.py b/src/snek/form/__init__.py index e69de29..5f25e13 100644 --- a/src/snek/form/__init__.py +++ b/src/snek/form/__init__.py @@ -0,0 +1,3 @@ +# retoor + + diff --git a/src/snek/form/login.py b/src/snek/form/login.py index ef13d67..19cdcc1 100644 --- a/src/snek/form/login.py +++ b/src/snek/form/login.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement diff --git a/src/snek/form/register.py b/src/snek/form/register.py index b105696..51fa934 100644 --- a/src/snek/form/register.py +++ b/src/snek/form/register.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement diff --git a/src/snek/form/search_user.py b/src/snek/form/search_user.py index 7e946b9..944ada5 100644 --- a/src/snek/form/search_user.py +++ b/src/snek/form/search_user.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement diff --git a/src/snek/form/settings/profile.py b/src/snek/form/settings/profile.py index 836cd67..7f45877 100644 --- a/src/snek/form/settings/profile.py +++ b/src/snek/form/settings/profile.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement diff --git a/src/snek/forum.py b/src/snek/forum.py index c05a6ae..6071c51 100644 --- a/src/snek/forum.py +++ b/src/snek/forum.py @@ -1,3 +1,5 @@ +# retoor + # forum_app.py import aiohttp.web from snek.view.forum import ForumIndexView, ForumView, ForumWebSocketView diff --git a/src/snek/gunicorn.py b/src/snek/gunicorn.py index 8583142..b359fe7 100644 --- a/src/snek/gunicorn.py +++ b/src/snek/gunicorn.py @@ -1,3 +1,5 @@ +# retoor + from snek.app import app application = app diff --git a/src/snek/mapper/__init__.py b/src/snek/mapper/__init__.py index c63921e..b707c80 100644 --- a/src/snek/mapper/__init__.py +++ b/src/snek/mapper/__init__.py @@ -1,3 +1,5 @@ +# retoor + import functools from snek.mapper.channel import ChannelMapper diff --git a/src/snek/mapper/channel.py b/src/snek/mapper/channel.py index 6239dc8..1b8acaf 100644 --- a/src/snek/mapper/channel.py +++ b/src/snek/mapper/channel.py @@ -1,3 +1,5 @@ +# retoor + from snek.model.channel import ChannelModel from snek.system.mapper import BaseMapper diff --git a/src/snek/mapper/channel_attachment.py b/src/snek/mapper/channel_attachment.py index 0d6e404..b8b02ea 100644 --- a/src/snek/mapper/channel_attachment.py +++ b/src/snek/mapper/channel_attachment.py @@ -1,3 +1,5 @@ +# retoor + from snek.model.channel_attachment import ChannelAttachmentModel from snek.system.mapper import BaseMapper diff --git a/src/snek/mapper/channel_member.py b/src/snek/mapper/channel_member.py index f0f62d6..76eed42 100644 --- a/src/snek/mapper/channel_member.py +++ b/src/snek/mapper/channel_member.py @@ -1,3 +1,5 @@ +# retoor + from snek.model.channel_member import ChannelMemberModel from snek.system.mapper import BaseMapper diff --git a/src/snek/mapper/channel_message.py b/src/snek/mapper/channel_message.py index 35ccbe9..1588292 100644 --- a/src/snek/mapper/channel_message.py +++ b/src/snek/mapper/channel_message.py @@ -1,3 +1,5 @@ +# retoor + from snek.model.channel_message import ChannelMessageModel from snek.system.mapper import BaseMapper diff --git a/src/snek/mapper/container.py b/src/snek/mapper/container.py index c2a089f..b3e0262 100644 --- a/src/snek/mapper/container.py +++ b/src/snek/mapper/container.py @@ -1,3 +1,5 @@ +# retoor + from snek.model.container import Container from snek.system.mapper import BaseMapper diff --git a/src/snek/mapper/drive.py b/src/snek/mapper/drive.py index c92c687..0f1747c 100644 --- a/src/snek/mapper/drive.py +++ b/src/snek/mapper/drive.py @@ -1,3 +1,5 @@ +# retoor + from snek.model.drive import DriveModel from snek.system.mapper import BaseMapper diff --git a/src/snek/mapper/drive_item.py b/src/snek/mapper/drive_item.py index 3d17a61..1e1651f 100644 --- a/src/snek/mapper/drive_item.py +++ b/src/snek/mapper/drive_item.py @@ -1,3 +1,5 @@ +# retoor + from snek.model.drive_item import DriveItemModel from snek.system.mapper import BaseMapper diff --git a/src/snek/mapper/forum.py b/src/snek/mapper/forum.py index ec258a2..47d55d9 100644 --- a/src/snek/mapper/forum.py +++ b/src/snek/mapper/forum.py @@ -1,3 +1,5 @@ +# retoor + # mapper/forum.py from snek.model.forum import ForumModel, ThreadModel, PostModel, PostLikeModel from snek.system.mapper import BaseMapper diff --git a/src/snek/mapper/notification.py b/src/snek/mapper/notification.py index 9bd74b5..316eb55 100644 --- a/src/snek/mapper/notification.py +++ b/src/snek/mapper/notification.py @@ -1,3 +1,5 @@ +# retoor + from snek.model.notification import NotificationModel from snek.system.mapper import BaseMapper diff --git a/src/snek/mapper/profile_page.py b/src/snek/mapper/profile_page.py index ec73b34..96043a5 100644 --- a/src/snek/mapper/profile_page.py +++ b/src/snek/mapper/profile_page.py @@ -1,3 +1,5 @@ +# retoor + import logging from snek.model.profile_page import ProfilePageModel from snek.system.mapper import BaseMapper diff --git a/src/snek/mapper/push.py b/src/snek/mapper/push.py index 2fe7c29..23b200c 100644 --- a/src/snek/mapper/push.py +++ b/src/snek/mapper/push.py @@ -1,3 +1,5 @@ +# retoor + from snek.model.push_registration import PushRegistrationModel from snek.system.mapper import BaseMapper diff --git a/src/snek/mapper/repository.py b/src/snek/mapper/repository.py index 1ac10d4..23a2825 100644 --- a/src/snek/mapper/repository.py +++ b/src/snek/mapper/repository.py @@ -1,3 +1,5 @@ +# retoor + from snek.model.repository import RepositoryModel from snek.system.mapper import BaseMapper diff --git a/src/snek/mapper/user.py b/src/snek/mapper/user.py index e0df494..693d274 100644 --- a/src/snek/mapper/user.py +++ b/src/snek/mapper/user.py @@ -1,6 +1,12 @@ +# retoor + +import logging + from snek.model.user import UserModel from snek.system.mapper import BaseMapper +logger = logging.getLogger(__name__) + class UserMapper(BaseMapper): table_name = "user" @@ -16,5 +22,5 @@ class UserMapper(BaseMapper): ) ] except Exception as ex: - print(ex) + logger.warning(f"Failed to get admin uids: {ex}") return [] diff --git a/src/snek/mapper/user_property.py b/src/snek/mapper/user_property.py index 7359f60..c671b25 100644 --- a/src/snek/mapper/user_property.py +++ b/src/snek/mapper/user_property.py @@ -1,3 +1,5 @@ +# retoor + from snek.model.user_property import UserPropertyModel from snek.system.mapper import BaseMapper diff --git a/src/snek/model/__init__.py b/src/snek/model/__init__.py index 13ba5b2..71d9f3a 100644 --- a/src/snek/model/__init__.py +++ b/src/snek/model/__init__.py @@ -1,3 +1,5 @@ +# retoor + import functools from snek.model.channel import ChannelModel @@ -44,5 +46,3 @@ def get_models(): def get_model(name): return get_models()[name] - - diff --git a/src/snek/model/channel.py b/src/snek/model/channel.py index e1ea1dd..9f1fab1 100644 --- a/src/snek/model/channel.py +++ b/src/snek/model/channel.py @@ -1,3 +1,5 @@ +# retoor + from snek.model.channel_message import ChannelMessageModel from snek.system.model import BaseModel, ModelField @@ -26,9 +28,8 @@ class ChannelModel(BaseModel): "SELECT uid FROM channel_message WHERE channel_uid=:channel_uid" + history_start_filter + " ORDER BY id DESC LIMIT 1", {"channel_uid": self["uid"]}, ): - return await self.app.services.channel_message.get(uid=model["uid"]) - except: + except Exception: pass return None diff --git a/src/snek/model/channel_attachment.py b/src/snek/model/channel_attachment.py index 0c0fba2..36b1a76 100644 --- a/src/snek/model/channel_attachment.py +++ b/src/snek/model/channel_attachment.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.model import BaseModel, ModelField diff --git a/src/snek/model/channel_member.py b/src/snek/model/channel_member.py index 54b0418..c902f7f 100644 --- a/src/snek/model/channel_member.py +++ b/src/snek/model/channel_member.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.model import BaseModel, ModelField diff --git a/src/snek/model/channel_message.py b/src/snek/model/channel_message.py index e3933e3..a778c17 100644 --- a/src/snek/model/channel_message.py +++ b/src/snek/model/channel_message.py @@ -1,3 +1,5 @@ +# retoor + from datetime import datetime, timezone from snek.model.user import UserModel diff --git a/src/snek/model/container.py b/src/snek/model/container.py index ee64144..4078ca4 100644 --- a/src/snek/model/container.py +++ b/src/snek/model/container.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.model import BaseModel, ModelField diff --git a/src/snek/model/drive.py b/src/snek/model/drive.py index df17d0f..4639b14 100644 --- a/src/snek/model/drive.py +++ b/src/snek/model/drive.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.model import BaseModel, ModelField diff --git a/src/snek/model/drive_item.py b/src/snek/model/drive_item.py index 82567a6..60c39a0 100644 --- a/src/snek/model/drive_item.py +++ b/src/snek/model/drive_item.py @@ -1,3 +1,5 @@ +# retoor + import mimetypes from snek.system.model import BaseModel, ModelField diff --git a/src/snek/model/forum.py b/src/snek/model/forum.py index 9c037a1..22e3faa 100644 --- a/src/snek/model/forum.py +++ b/src/snek/model/forum.py @@ -1,3 +1,5 @@ +# retoor + # models/forum.py from snek.system.model import BaseModel, ModelField diff --git a/src/snek/model/notification.py b/src/snek/model/notification.py index 6a12328..7c19f55 100644 --- a/src/snek/model/notification.py +++ b/src/snek/model/notification.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.model import BaseModel, ModelField diff --git a/src/snek/model/profile_page.py b/src/snek/model/profile_page.py index 653615f..e80e5e3 100644 --- a/src/snek/model/profile_page.py +++ b/src/snek/model/profile_page.py @@ -1,3 +1,5 @@ +# retoor + import logging from snek.system.model import BaseModel, ModelField diff --git a/src/snek/model/push_registration.py b/src/snek/model/push_registration.py index bb13be0..2ab0151 100644 --- a/src/snek/model/push_registration.py +++ b/src/snek/model/push_registration.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.model import BaseModel, ModelField diff --git a/src/snek/model/repository.py b/src/snek/model/repository.py index 6fb0206..405b579 100644 --- a/src/snek/model/repository.py +++ b/src/snek/model/repository.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.model import BaseModel, ModelField diff --git a/src/snek/model/user.py b/src/snek/model/user.py index 93e83af..7327a8c 100644 --- a/src/snek/model/user.py +++ b/src/snek/model/user.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.model import BaseModel, ModelField diff --git a/src/snek/model/user_property.py b/src/snek/model/user_property.py index 27dc84f..5ced1fc 100644 --- a/src/snek/model/user_property.py +++ b/src/snek/model/user_property.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.model import BaseModel, ModelField diff --git a/src/snek/research/serpentarium.py b/src/snek/research/serpentarium.py index 481e218..57e1b54 100644 --- a/src/snek/research/serpentarium.py +++ b/src/snek/research/serpentarium.py @@ -1,6 +1,17 @@ +# retoor + +import asyncio import json + + class DatasetMethod: + pass + + class DatasetTable: + pass + + class WebSocketClient2: def __init__(self, uri): self.uri = uri @@ -8,25 +19,40 @@ class WebSocketClient2: self.websocket = None self.receive_queue = asyncio.Queue() - # Schedule connection setup - if self.loop.is_running(): - # Schedule connect in the existing loop - self._connect_future = asyncio.run_coroutine_threadsafe(self._connect(), self.loop) def send(self, message: str): + pass + def close(self): -class DatasetWrapper(object): + pass + + +class DatasetWrapper: def __init__(self): + pass + def commit(self): + pass + def query(self, *args, **kwargs): + pass + + class DatasetWebSocketView: def __init__(self): self.ws = None - self.db = dataset.connect('sqlite:///snek.db') - setattr(self, "db", self.get) - setattr(self, "db", self.set) + def format_result(self, result): - + pass + async def send_str(self, msg): + pass + def get(self, key): + pass + def set(self, key, value): + pass + + async def run_server(): + pass diff --git a/src/snek/research/serptest.py b/src/snek/research/serptest.py index 891cccc..58dc2da 100644 --- a/src/snek/research/serptest.py +++ b/src/snek/research/serptest.py @@ -1,3 +1,5 @@ +# retoor + import time from concurrent.futures import ProcessPoolExecutor diff --git a/src/snek/service/__init__.py b/src/snek/service/__init__.py index c9f75c6..e4e9dc0 100644 --- a/src/snek/service/__init__.py +++ b/src/snek/service/__init__.py @@ -1,3 +1,5 @@ +# retoor + import functools from snek.service.channel import ChannelService @@ -41,7 +43,7 @@ def get_services(app): def get_service(name, app=None): return get_services(app=app)[name] -# Registering all services + register_service("user", UserService) register_service("channel_member", ChannelMemberService) register_service("channel", ChannelService) diff --git a/src/snek/service/channel.py b/src/snek/service/channel.py index 5d15683..9c6315b 100644 --- a/src/snek/service/channel.py +++ b/src/snek/service/channel.py @@ -1,3 +1,5 @@ +# retoor + import pathlib from datetime import datetime @@ -13,7 +15,7 @@ class ChannelService(BaseService): if not folder.exists(): try: folder.mkdir(parents=True, exist_ok=True) - except: + except OSError: pass return folder diff --git a/src/snek/service/channel_attachment.py b/src/snek/service/channel_attachment.py index 755aaa0..7d1a5cc 100644 --- a/src/snek/service/channel_attachment.py +++ b/src/snek/service/channel_attachment.py @@ -1,3 +1,5 @@ +# retoor + import mimetypes from snek.system.service import BaseService diff --git a/src/snek/service/channel_member.py b/src/snek/service/channel_member.py index b9b88d8..58d24cb 100644 --- a/src/snek/service/channel_member.py +++ b/src/snek/service/channel_member.py @@ -1,5 +1,7 @@ -from snek.system.service import BaseService +# retoor + from snek.system.model import now +from snek.system.service import BaseService class ChannelMemberService(BaseService): @@ -14,7 +16,7 @@ class ChannelMemberService(BaseService): async def get_user_uids(self, channel_uid): async for model in self.mapper.query( - "SELECT user_uid FROM channel_member WHERE channel_uid=:channel_uid", + "SELECT user_uid FROM channel_member WHERE channel_uid=:channel_uid AND deleted_at IS NULL AND is_banned = 0", {"channel_uid": channel_uid}, ): yield model["user_uid"] @@ -58,7 +60,6 @@ class ChannelMemberService(BaseService): "SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') LEFT JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = NULL AND channel_member2.user_uid = NULL) WHERE channel_member.user_uid=:from_user ", {"from_user": from_user, "to_user": to_user}, ): - return model async def get_other_dm_user(self, channel_uid, user_uid): diff --git a/src/snek/service/channel_message.py b/src/snek/service/channel_message.py index cf65c0c..1de1e89 100644 --- a/src/snek/service/channel_message.py +++ b/src/snek/service/channel_message.py @@ -1,18 +1,21 @@ +# retoor + +import asyncio +import json +import logging +import pathlib +from concurrent.futures import ProcessPoolExecutor + from snek.system.service import BaseService from snek.system.template import sanitize_html -import time -import asyncio -from concurrent.futures import ProcessPoolExecutor -import json -from jinja2 import Environment, FileSystemLoader -global jinja2_env -import pathlib +logger = logging.getLogger(__name__) +jinja2_env = None template_path = pathlib.Path(__file__).parent.parent.joinpath("templates") def render(context): - template =jinja2_env.get_template("message.html") + template = jinja2_env.get_template("message.html") return sanitize_html(template.render(**context)) @@ -27,10 +30,11 @@ class ChannelMessageService(BaseService): global jinja2_env jinja2_env = self.app.jinja2_env self._max_workers = 1 + def get_or_create_executor(self, uid): if not uid in self._executor_pools: self._executor_pools[uid] = ProcessPoolExecutor(max_workers=self._max_workers) - print("Executors available", len(self._executor_pools)) + logger.debug(f"Executors available: {len(self._executor_pools)}") return self._executor_pools[uid] def delete_executor(self, uid): @@ -39,34 +43,6 @@ class ChannelMessageService(BaseService): del self._executor_pools[uid] async def maintenance(self): - args = {} - - - return - for message in self.mapper.db["channel_message"].find(): - print(message) - try: - message = await self.get(uid=message["uid"]) - updated_at = message["updated_at"] - message["is_final"] = True - html = message["html"] - await self.save(message) - - self.mapper.db["channel_message"].upsert( - { - "uid": message["uid"], - "updated_at": updated_at, - }, - ["uid"], - ) - if html != message["html"]: - print("Reredefined message", message["uid"]) - - except Exception as ex: - time.sleep(0.1) - print(ex, flush=True) - - while True: changed = 0 async for message in self.find(is_final=False): @@ -81,6 +57,7 @@ class ChannelMessageService(BaseService): break async def create(self, channel_uid, user_uid, message, is_final=True): + logger.info(f"create: channel_uid={channel_uid}, user_uid={user_uid}, message_len={len(message) if message else 0}, is_final={is_final}") model = await self.new() model["channel_uid"] = channel_uid @@ -93,6 +70,9 @@ class ChannelMessageService(BaseService): record = model.record context.update(record) user = await self.app.services.user.get(uid=user_uid) + if not user: + logger.error(f"create: user not found user_uid={user_uid}") + raise Exception("User not found") context.update( { "user_uid": user["uid"], @@ -103,15 +83,13 @@ class ChannelMessageService(BaseService): ) loop = asyncio.get_event_loop() try: - - context = json.loads(json.dumps(context, default=str)) - - + context = json.loads(json.dumps(context, default=str)) + logger.debug(f"create: rendering html for message uid={model['uid']}") model["html"] = await loop.run_in_executor(self.get_or_create_executor(model["uid"]), render,context) - #model['html'] = await loop.run_in_executor(self.get_or_create_executor(user["uid"]), sanitize_html,model['html']) except Exception as ex: - print(ex, flush=True) + logger.error(f"create: html rendering failed: {ex}") + logger.debug(f"create: saving message uid={model['uid']}") if await super().save(model): if not self._configured_indexes: if not self.mapper.db["channel_message"].has_index( @@ -129,7 +107,9 @@ class ChannelMessageService(BaseService): self._configured_indexes = True if model['is_final']: self.delete_executor(model['uid']) + logger.info(f"create: message created successfully uid={model['uid']}, channel={channel_uid}") return model + logger.error(f"create: failed to save message channel={channel_uid}, errors={model.errors}") raise Exception(f"Failed to create channel message: {model.errors}.") async def to_extended_dict(self, message): @@ -154,9 +134,13 @@ class ChannelMessageService(BaseService): } async def save(self, model): + logger.debug(f"save: starting for uid={model['uid']}, is_final={model['is_final']}") context = {} context.update(model.record) user = await self.app.services.user.get(model["user_uid"]) + if not user: + logger.error(f"save: user not found user_uid={model['user_uid']}") + return False context.update( { "user_uid": user["uid"], @@ -165,12 +149,16 @@ class ChannelMessageService(BaseService): "color": user["color"], } ) - context = json.loads(json.dumps(context, default=str)) + context = json.loads(json.dumps(context, default=str)) loop = asyncio.get_event_loop() + logger.debug(f"save: rendering html for uid={model['uid']}") model["html"] = await loop.run_in_executor(self.get_or_create_executor(model["uid"]), render, context) - #model['html'] = await loop.run_in_executor(self.get_or_create_executor(user["uid"]), sanitize_html,model['html']) result = await super().save(model) + if result: + logger.debug(f"save: message saved successfully uid={model['uid']}") + else: + logger.warning(f"save: failed to save message uid={model['uid']}") if model['is_final']: self.delete_executor(model['uid']) return result @@ -219,6 +207,6 @@ class ChannelMessageService(BaseService): results.append(model) except Exception as ex: - print(ex) + logger.error(f"offset query failed: {ex}") results.sort(key=lambda x: x["created_at"]) return results diff --git a/src/snek/service/chat.py b/src/snek/service/chat.py index c62b795..03cebc7 100644 --- a/src/snek/service/chat.py +++ b/src/snek/service/chat.py @@ -1,17 +1,29 @@ +# retoor + +import logging + from snek.system.model import now from snek.system.service import BaseService +logger = logging.getLogger(__name__) + class ChatService(BaseService): async def finalize(self, message_uid): + logger.info(f"finalize: starting for message_uid={message_uid}") channel_message = await self.services.channel_message.get(uid=message_uid) + if not channel_message: + logger.warning(f"finalize: message not found uid={message_uid}") + return channel_message["is_final"] = True await self.services.channel_message.save(channel_message) + logger.debug(f"finalize: message marked as final uid={message_uid}") user = await self.services.user.get(uid=channel_message["user_uid"]) channel = await self.services.channel.get(uid=channel_message["channel_uid"]) channel["last_message_on"] = now() await self.services.channel.save(channel) + logger.debug(f"finalize: broadcasting message to channel={channel['uid']}") await self.services.socket.broadcast( channel["uid"], { @@ -28,18 +40,23 @@ class ChatService(BaseService): "is_final": channel_message["is_final"], }, ) + logger.info(f"finalize: completed for message_uid={message_uid}, channel={channel['uid']}") await self.app.create_task( self.services.notification.create_channel_message(message_uid) ) async def send(self, user_uid, channel_uid, message, is_final=True): + logger.info(f"send: user_uid={user_uid}, channel_uid={channel_uid}, message_len={len(message) if message else 0}, is_final={is_final}") channel = await self.services.channel.get(uid=channel_uid) if not channel: + logger.error(f"send: channel not found channel_uid={channel_uid}") raise Exception("Channel not found.") + logger.debug(f"send: checking for existing non-final message in channel={channel_uid}") channel_message = await self.services.channel_message.get( channel_uid=channel_uid,user_uid=user_uid, is_final=False ) if channel_message: + logger.debug(f"send: updating existing message uid={channel_message['uid']}") channel_message["message"] = message channel_message["is_final"] = is_final if not channel_message["is_final"]: @@ -48,15 +65,18 @@ class ChatService(BaseService): else: await self.services.channel_message.save(channel_message) else: + logger.debug(f"send: creating new message in channel={channel_uid}") channel_message = await self.services.channel_message.create( channel_uid, user_uid, message, is_final ) channel_message_uid = channel_message["uid"] + logger.debug(f"send: message saved uid={channel_message_uid}") user = await self.services.user.get(uid=user_uid) channel["last_message_on"] = now() await self.services.channel.save(channel) + logger.debug(f"send: broadcasting message to channel={channel_uid}") await self.services.socket.broadcast( channel_uid, { @@ -73,6 +93,7 @@ class ChatService(BaseService): "is_final": is_final, }, ) + logger.info(f"send: completed message_uid={channel_message_uid}, channel={channel_uid}, is_final={is_final}") await self.app.create_task( self.services.notification.create_channel_message(channel_message_uid) ) diff --git a/src/snek/service/container.py b/src/snek/service/container.py index fba5484..62faa53 100644 --- a/src/snek/service/container.py +++ b/src/snek/service/container.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.docker import ComposeFileManager from snek.system.service import BaseService @@ -116,4 +118,3 @@ class ContainerService(BaseService): if await super().save(model): return model raise Exception(f"Failed to create container: {model.errors}") - diff --git a/src/snek/service/db.py b/src/snek/service/db.py index 8e583e9..c619e20 100644 --- a/src/snek/service/db.py +++ b/src/snek/service/db.py @@ -1,3 +1,5 @@ +# retoor + import dataset from snek.system.service import BaseService diff --git a/src/snek/service/drive.py b/src/snek/service/drive.py index 38035c7..24bcd6d 100644 --- a/src/snek/service/drive.py +++ b/src/snek/service/drive.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.service import BaseService diff --git a/src/snek/service/drive_item.py b/src/snek/service/drive_item.py index 7a0c59a..e3930fa 100644 --- a/src/snek/service/drive_item.py +++ b/src/snek/service/drive_item.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.service import BaseService diff --git a/src/snek/service/forum.py b/src/snek/service/forum.py index 79cf15d..30d6578 100644 --- a/src/snek/service/forum.py +++ b/src/snek/service/forum.py @@ -1,3 +1,5 @@ +# retoor + # services/forum.py from snek.system.service import BaseService import re diff --git a/src/snek/service/notification.py b/src/snek/service/notification.py index fa3cdc3..fca652d 100644 --- a/src/snek/service/notification.py +++ b/src/snek/service/notification.py @@ -1,7 +1,13 @@ +# retoor + +import logging + from snek.system.markdown import strip_markdown from snek.system.model import now from snek.system.service import BaseService +logger = logging.getLogger(__name__) + class NotificationService(BaseService): mapper_name = "notification" @@ -79,6 +85,6 @@ class NotificationService(BaseService): }, ) except Exception as e: - print(f"Failed to send push notification:", e) + logger.warning(f"Failed to send push notification: {e}") self.app.db.commit() diff --git a/src/snek/service/profile_page.py b/src/snek/service/profile_page.py index 7d0807b..0931b87 100644 --- a/src/snek/service/profile_page.py +++ b/src/snek/service/profile_page.py @@ -1,3 +1,5 @@ +# retoor + import logging import re from typing import Optional, List diff --git a/src/snek/service/push.py b/src/snek/service/push.py index d032c05..fe74c5b 100644 --- a/src/snek/service/push.py +++ b/src/snek/service/push.py @@ -1,3 +1,5 @@ +# retoor + import base64 import json import os.path diff --git a/src/snek/service/repository.py b/src/snek/service/repository.py index 0cd1ec5..cf40fc6 100644 --- a/src/snek/service/repository.py +++ b/src/snek/service/repository.py @@ -1,3 +1,5 @@ +# retoor + import asyncio import shutil diff --git a/src/snek/service/socket.py b/src/snek/service/socket.py index 251511f..1cf4b9c 100644 --- a/src/snek/service/socket.py +++ b/src/snek/service/socket.py @@ -16,8 +16,11 @@ def safe_get(obj, key, default=None): try: if isinstance(obj, dict): return obj.get(key, default) + if hasattr(obj, "fields") and hasattr(obj, "__getitem__"): + val = obj[key] + return val if val is not None else default return getattr(obj, key, default) - except Exception: + except (KeyError, TypeError, AttributeError): return default @@ -195,7 +198,9 @@ class SocketService(BaseService): async def broadcast(self, channel_uid, message): if not channel_uid or message is None: + logger.debug(f"broadcast: invalid params channel_uid={channel_uid}, message={message is not None}") return False + logger.debug(f"broadcast: starting for channel={channel_uid}") return await self._broadcast(channel_uid, message) async def _broadcast(self, channel_uid, message): @@ -209,18 +214,23 @@ class SocketService(BaseService): async for user_uid in self.services.channel_member.get_user_uids(channel_uid): if user_uid: user_uids_to_send.add(user_uid) + logger.debug(f"_broadcast: found {len(user_uids_to_send)} users from db for channel={channel_uid}") except Exception as ex: logger.warning(f"Broadcast db query failed: {safe_str(ex)}") if not user_uids_to_send: async with self._lock: if channel_uid in self.subscriptions: user_uids_to_send = set(self.subscriptions[channel_uid]) + logger.debug(f"_broadcast: using {len(user_uids_to_send)} users from subscriptions for channel={channel_uid}") for user_uid in user_uids_to_send: try: - sent += await self.send_to_user(user_uid, message) + count = await self.send_to_user(user_uid, message) + sent += count + if count > 0: + logger.debug(f"_broadcast: sent to user={user_uid}, sockets={count}") except Exception as ex: logger.debug(f"Failed to send to user {user_uid}: {safe_str(ex)}") - logger.debug(f"Broadcasted a message to {sent} users.") + logger.info(f"_broadcast: completed channel={channel_uid}, total_users={len(user_uids_to_send)}, sent={sent}") return True except Exception as ex: logger.warning(f"Broadcast failed: {safe_str(ex)}") diff --git a/src/snek/service/statistics.py b/src/snek/service/statistics.py index 7742030..c160369 100644 --- a/src/snek/service/statistics.py +++ b/src/snek/service/statistics.py @@ -1,5 +1,9 @@ -from snek.system.service import BaseService -import sqlite3 +# retoor + +import sqlite3 + +from snek.system.service import BaseService + class StatisticsService(BaseService): diff --git a/src/snek/service/user.py b/src/snek/service/user.py index 16b317b..4f96dff 100644 --- a/src/snek/service/user.py +++ b/src/snek/service/user.py @@ -1,3 +1,5 @@ +# retoor + import pathlib from snek.system import security @@ -78,7 +80,7 @@ class UserService(BaseService): if not folder.exists(): try: folder.mkdir(parents=True, exist_ok=True) - except: + except OSError: pass return folder @@ -87,7 +89,7 @@ class UserService(BaseService): if not folder.exists(): try: folder.mkdir(parents=True, exist_ok=True) - except: + except OSError: pass return folder diff --git a/src/snek/service/user_property.py b/src/snek/service/user_property.py index 23eb9cc..544ec3c 100644 --- a/src/snek/service/user_property.py +++ b/src/snek/service/user_property.py @@ -1,3 +1,5 @@ +# retoor + import json from snek.system.service import BaseService diff --git a/src/snek/service/util.py b/src/snek/service/util.py index b620d9c..e2c7ec4 100644 --- a/src/snek/service/util.py +++ b/src/snek/service/util.py @@ -1,3 +1,5 @@ +# retoor + import random from snek.system.service import BaseService diff --git a/src/snek/sgit.py b/src/snek/sgit.py index 7e1adeb..b225911 100644 --- a/src/snek/sgit.py +++ b/src/snek/sgit.py @@ -1,3 +1,5 @@ +# retoor + import asyncio import base64 import json diff --git a/src/snek/shell.py b/src/snek/shell.py index 2a1546b..8ae616a 100644 --- a/src/snek/shell.py +++ b/src/snek/shell.py @@ -1,3 +1,5 @@ +# retoor + from snek.app import Application from IPython import start_ipython diff --git a/src/snek/snode.py b/src/snek/snode.py index 517d52a..2919736 100644 --- a/src/snek/snode.py +++ b/src/snek/snode.py @@ -1,3 +1,5 @@ +# retoor + import aiohttp ENABLED = False diff --git a/src/snek/sssh.py b/src/snek/sssh.py index bb83fa9..f12feb0 100644 --- a/src/snek/sssh.py +++ b/src/snek/sssh.py @@ -1,3 +1,5 @@ +# retoor + import logging from pathlib import Path diff --git a/src/snek/static/app.js b/src/snek/static/app.js index 7126506..ca4df7a 100644 --- a/src/snek/static/app.js +++ b/src/snek/static/app.js @@ -1,11 +1,4 @@ -// Written by retoor@molodetz.nl - -// This project implements a client-server communication system using WebSockets and REST APIs. -// It features a chat system, a notification sound system, and interaction with server endpoints. - -// No additional imports were used beyond standard JavaScript objects and constructors. - -// MIT License +// retoor import { Schedule } from "./schedule.js"; import { EventHandler } from "./event-handler.js"; @@ -164,7 +157,7 @@ export class App extends EventHandler { _debug = false; presenceNotification = null; async set_typing(channel_uid) { - this.typeEventChannel_uid = channel_uid; + this.typeEventChannelUid = channel_uid; } debug() { this._debug = !this._debug; @@ -176,17 +169,8 @@ export class App extends EventHandler { await this.rpc.ping(...args); this.is_pinging = false; } - ntsh(times,message) { - if(!message) - message = "Nothing to see here!" - if(!times) - times=100 - for(let x = 0; x < times; x++){ - this.rpc.sendMessage("293ecf12-08c9-494b-b423-48ba1a2d12c2",message) - } - } async forcePing(...arg) { - await this.rpc.ping(...args); + await this.rpc.ping(...arg); } starField = null constructor() { @@ -205,7 +189,7 @@ export class App extends EventHandler { this.rpc.set_typing(this.typeEventChannelUid); this.typeEventChannelUid = null; } - }); + }, 1000); const me = this; this.ws.addEventListener("connected", (data) => { diff --git a/src/snek/static/chat-input.js b/src/snek/static/chat-input.js index 4fb5a7a..6d8de3d 100644 --- a/src/snek/static/chat-input.js +++ b/src/snek/static/chat-input.js @@ -1,6 +1,11 @@ +// retoor + import { app } from "./app.js"; -import { NjetComponent,eventBus } from "./njet.js"; +import { NjetComponent, eventBus } from "./njet.js"; import { FileUploadGrid } from "./file-upload-grid.js"; +import { loggerFactory } from "./logger.js"; + +const log = loggerFactory.getLogger("ChatInput"); class ChatInputComponent extends NjetComponent { autoCompletions = { @@ -504,7 +509,10 @@ textToLeetAdvanced(text) { flagTyping() { if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) >= 1) { this.lastUpdateEvent = new Date(); - app.rpc.set_typing(this.channelUid, this.user?.color).catch(() => {}); + log.debug("Flagging typing indicator", { channelUid: this.channelUid }); + app.rpc.set_typing(this.channelUid, this.user?.color).catch((e) => { + log.warn("set_typing failed", { error: e, channelUid: this.channelUid }); + }); } } @@ -516,7 +524,12 @@ textToLeetAdvanced(text) { }else if(this._leetSpeakAdvanced){ value = this.textToLeetAdvanced(value); } - app.rpc.sendMessage(this.channelUid, value , true); + log.info("Finalizing message", { channelUid: this.channelUid, messageLength: value.length, messageUid }); + app.rpc.sendMessage(this.channelUid, value , true).then((result) => { + log.debug("Message finalized successfully", { channelUid: this.channelUid, result }); + }).catch((e) => { + log.error("Failed to finalize message", { channelUid: this.channelUid, error: e }); + }); this.value = ""; this.messageUid = null; this.queuedMessage = null; @@ -526,6 +539,7 @@ textToLeetAdvanced(text) { updateFromInput(value, isFinal = false) { + log.debug("updateFromInput called", { valueLength: value?.length, isFinal, liveType: this.liveType, channelUid: this.channelUid }); this.value = value; @@ -533,13 +547,22 @@ textToLeetAdvanced(text) { if (this.liveType && value[0] !== "/") { const messageText = this.replaceMentionsWithAuthors(value); + log.debug("Sending live type message", { channelUid: this.channelUid, messageLength: messageText?.length, isFinal: !this.liveType || isFinal }); this.messageUid = this.sendMessage(this.channelUid, messageText, !this.liveType || isFinal); return this.messageUid; } } async sendMessage(channelUid, value, is_final) { - return await app.rpc.sendMessage(channelUid, value, is_final); + log.info("sendMessage called", { channelUid, valueLength: value?.length, is_final }); + try { + const result = await app.rpc.sendMessage(channelUid, value, is_final); + log.debug("sendMessage completed", { channelUid, result, is_final }); + return result; + } catch (e) { + log.error("sendMessage failed", { channelUid, error: e, is_final }); + throw e; + } } } diff --git a/src/snek/static/chat-window.js b/src/snek/static/chat-window.js index d1bbb56..0646ba2 100644 --- a/src/snek/static/chat-window.js +++ b/src/snek/static/chat-window.js @@ -1,3 +1,5 @@ +// retoor + // Written by retoor@molodetz.nl // This code defines a custom HTML element called ChatWindowElement that provides a chat interface within a shadow DOM, handling connection callbacks, displaying messages, and user interactions. diff --git a/src/snek/static/container.js b/src/snek/static/container.js index 3261184..f7ee6ee 100644 --- a/src/snek/static/container.js +++ b/src/snek/static/container.js @@ -1,3 +1,5 @@ +// retoor + import { app } from "./app.js"; import { EventHandler } from "./event-handler.js"; @@ -30,7 +32,7 @@ export class Container extends EventHandler{ } refresh(){ //this._fitAddon.fit(); - this.ws.send("\x0C"); + this.ws.send(" "); } toggle(){ this._container.classList.toggle("hidden") @@ -110,4 +112,3 @@ export class Container extends EventHandler{ window.getContainer = function(){ return new Container(app.channelUid) }*/ - diff --git a/src/snek/static/dumb-term.js b/src/snek/static/dumb-term.js index 6558ee4..6aaab69 100644 --- a/src/snek/static/dumb-term.js +++ b/src/snek/static/dumb-term.js @@ -1,3 +1,5 @@ +// retoor + import { NjetComponent } from "/njet.js"; class WebTerminal extends NjetComponent { @@ -247,4 +249,3 @@ window.showTerm = function (options) { customElements.define("web-terminal", WebTerminal); export { WebTerminal }; - diff --git a/src/snek/static/editor.js b/src/snek/static/editor.js index d1b3ae1..eb3f084 100644 --- a/src/snek/static/editor.js +++ b/src/snek/static/editor.js @@ -1,3 +1,5 @@ +// retoor + import { NjetComponent } from "/njet.js" class NjetEditor extends NjetComponent { @@ -266,7 +268,8 @@ Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`; } goToLine(lineNum) { - const lines = this.editor.innerText.split('\n'); + const lines = this.editor.innerText.split(' +'); if (lineNum < 0 || lineNum >= lines.length) return; let offset = 0; @@ -279,7 +282,8 @@ Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`; getCurrentLineInfo() { const text = this.editor.innerText; const caretPos = this.getCaretOffset(); - const lines = text.split('\n'); + const lines = text.split(' +'); let charCount = 0; for (let i = 0; i < lines.length; i++) { @@ -405,7 +409,8 @@ Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`; this.lastDeletedLine = lines[lineIndex]; lines.splice(lineIndex, 1); if (lines.length === 0) lines.push(''); - this.editor.innerText = lines.join('\n'); + this.editor.innerText = lines.join(' +'); this.setCaretOffset(lineStartOffset); break; @@ -414,7 +419,8 @@ Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`; const lineToPaste = this.yankedLine || this.lastDeletedLine; if (lineToPaste) { lines.splice(lineIndex + 1, 0, lineToPaste); - this.editor.innerText = lines.join('\n'); + this.editor.innerText = lines.join(' +'); this.setCaretOffset(lineStartOffset + lines[lineIndex].length + 1); } break; diff --git a/src/snek/static/fancy-button.js b/src/snek/static/fancy-button.js index b23233c..acdbb02 100644 --- a/src/snek/static/fancy-button.js +++ b/src/snek/static/fancy-button.js @@ -1,3 +1,5 @@ +// retoor + // Written by retoor@molodetz.nl // This JavaScript class defines a custom HTML element , which creates a styled, clickable button element with customizable size, text, and URL redirect functionality. diff --git a/src/snek/static/file-manager.js b/src/snek/static/file-manager.js index 8cd768c..956c14e 100644 --- a/src/snek/static/file-manager.js +++ b/src/snek/static/file-manager.js @@ -1,3 +1,5 @@ +// retoor + /* A  custom element that talks to /api/files */ import { NjetComponent } from "/njet.js"; diff --git a/src/snek/static/file-upload-grid.js b/src/snek/static/file-upload-grid.js index 8cdd4e9..a4968af 100644 --- a/src/snek/static/file-upload-grid.js +++ b/src/snek/static/file-upload-grid.js @@ -1,3 +1,5 @@ +// retoor + import { NjetComponent, NjetDialog } from '/njet.js'; const FUG_ICONS = { diff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js index e2d6f29..e5da70b 100644 --- a/src/snek/static/generic-form.js +++ b/src/snek/static/generic-form.js @@ -1,3 +1,5 @@ +// retoor + // Written by retoor@molodetz.nl // This code defines two custom HTML elements, `GenericField` and `GenericForm`. The `GenericField` element represents a form field with validation and styling functionalities, and the `GenericForm` fetches and manages form data, handling field validation and submission. diff --git a/src/snek/static/html-frame.js b/src/snek/static/html-frame.js index 78869a5..b980ecf 100644 --- a/src/snek/static/html-frame.js +++ b/src/snek/static/html-frame.js @@ -1,3 +1,5 @@ +// retoor + // Written by retoor@molodetz.nl // The following JavaScript code defines a custom HTML element `` that loads and displays HTML content from a specified URL. If the URL is provided as a markdown file, it attempts to render it as HTML. diff --git a/src/snek/static/logger.js b/src/snek/static/logger.js new file mode 100644 index 0000000..66e22e1 --- /dev/null +++ b/src/snek/static/logger.js @@ -0,0 +1,98 @@ +// retoor + +const LogLevel = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, +}; + +class Logger { + constructor(name, level = LogLevel.DEBUG) { + this.name = name; + this.level = level; + this.enabled = true; + } + + _format(level, message, data) { + const timestamp = new Date().toISOString(); + const prefix = `[${timestamp}] [${level}] [${this.name}]`; + return { prefix, message, data }; + } + + _log(level, levelName, message, data) { + if (!this.enabled || level < this.level) return; + const { prefix } = this._format(levelName, message, data); + const logFn = level === LogLevel.ERROR ? console.error : + level === LogLevel.WARN ? console.warn : + level === LogLevel.INFO ? console.info : console.debug; + if (data !== undefined) { + logFn(`${prefix} ${message}`, data); + } else { + logFn(`${prefix} ${message}`); + } + } + + debug(message, data) { + this._log(LogLevel.DEBUG, "DEBUG", message, data); + } + + info(message, data) { + this._log(LogLevel.INFO, "INFO", message, data); + } + + warn(message, data) { + this._log(LogLevel.WARN, "WARN", message, data); + } + + error(message, data) { + this._log(LogLevel.ERROR, "ERROR", message, data); + } + + setLevel(level) { + this.level = level; + } + + enable() { + this.enabled = true; + } + + disable() { + this.enabled = false; + } +} + +class LoggerFactory { + constructor() { + this.loggers = new Map(); + this.globalLevel = LogLevel.DEBUG; + this.globalEnabled = true; + } + + getLogger(name) { + if (!this.loggers.has(name)) { + const logger = new Logger(name, this.globalLevel); + logger.enabled = this.globalEnabled; + this.loggers.set(name, logger); + } + return this.loggers.get(name); + } + + setGlobalLevel(level) { + this.globalLevel = level; + this.loggers.forEach((logger) => logger.setLevel(level)); + } + + enableAll() { + this.globalEnabled = true; + this.loggers.forEach((logger) => logger.enable()); + } + + disableAll() { + this.globalEnabled = false; + this.loggers.forEach((logger) => logger.disable()); + } +} + +export const loggerFactory = new LoggerFactory(); +export { Logger, LogLevel }; diff --git a/src/snek/static/markdown-frame.js b/src/snek/static/markdown-frame.js index 504cc5d..8e35f15 100644 --- a/src/snek/static/markdown-frame.js +++ b/src/snek/static/markdown-frame.js @@ -1,3 +1,5 @@ +// retoor + // Written by retoor@molodetz.nl // This JavaScript class defines a custom HTML element that fetches and loads content from a specified URL into a shadow DOM. diff --git a/src/snek/static/media-upload.js b/src/snek/static/media-upload.js index c4e5966..3dd5e00 100644 --- a/src/snek/static/media-upload.js +++ b/src/snek/static/media-upload.js @@ -1,3 +1,5 @@ +// retoor + // Written by retoor@molodetz.nl // This code defines custom web components to create and interact with a tile grid system for displaying images, along with an upload button to facilitate image additions. diff --git a/src/snek/static/message-list-manager.js b/src/snek/static/message-list-manager.js index 28ebf18..24a3085 100644 --- a/src/snek/static/message-list-manager.js +++ b/src/snek/static/message-list-manager.js @@ -1,3 +1,5 @@ +// retoor + // Written by retoor@molodetz.nl // This JavaScript source code defines a custom HTML element named "message-list-manager" to manage a list of message lists for different channels obtained asynchronously. diff --git a/src/snek/static/message-list.js b/src/snek/static/message-list.js index 344eeae..00a58ff 100644 --- a/src/snek/static/message-list.js +++ b/src/snek/static/message-list.js @@ -1,10 +1,5 @@ -// Written by retoor@molodetz.nl +// retoor -// This class defines a custom HTML element that displays a list of messages with avatars and timestamps. It handles message addition with a delay in event dispatch and ensures the display of messages in the correct format. - -// The code seems to rely on some external dependencies like 'models.Message', 'app', and 'Schedule'. These should be imported or defined elsewhere in your application. - -// MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty. import { app } from "./app.js"; const LONG_TIME = 1000 * 60 * 20; @@ -182,6 +177,7 @@ class MessageList extends HTMLElement { } }); app.ws.addEventListener("set_typing", (data) => { + if (app._debug) console.debug("set_typing event received:", data); this.triggerGlow(data.user_uid, data.color); }); @@ -256,8 +252,8 @@ class MessageList extends HTMLElement { } triggerGlow(uid, color) { - if (!uid || !color) return; - if (app.starField) app.starField.glowColor(color); + if (color && app.starField) app.starField.glowColor(color); + if (!uid) return; let lastElement = null; this.querySelectorAll('.avatar').forEach((el) => { const anchor = el.closest('a'); @@ -285,12 +281,6 @@ class MessageList extends HTMLElement { upsertMessage(data) { let message = this.messageMap.get(data.uid); - if (message && (data.is_final || !data.message)) { - //message.parentElement?.removeChild(message); - // TO force insert - //message = null; - - } if(message && !data.message){ message.parentElement?.removeChild(message); message = null; diff --git a/src/snek/static/models.js b/src/snek/static/models.js index b836c44..87b9dd2 100644 --- a/src/snek/static/models.js +++ b/src/snek/static/models.js @@ -1,3 +1,5 @@ +// retoor + // Written by retoor@molodetz.nl // This code defines a class 'MessageModel' representing a message entity with various properties such as user and channel IDs, message content, and timestamps. It includes a constructor to initialize these properties. diff --git a/src/snek/static/njet.js b/src/snek/static/njet.js index 880f74a..d539229 100644 --- a/src/snek/static/njet.js +++ b/src/snek/static/njet.js @@ -1,3 +1,5 @@ +// retoor + class RestClient { constructor({ baseURL = '', headers = {} } = {}) { this.baseURL = baseURL; diff --git a/src/snek/static/online-users.js b/src/snek/static/online-users.js index e69de29..f15ac02 100644 --- a/src/snek/static/online-users.js +++ b/src/snek/static/online-users.js @@ -0,0 +1,3 @@ +// retoor + + diff --git a/src/snek/static/polyfills/Promise.withResolvers.js b/src/snek/static/polyfills/Promise.withResolvers.js index 03f0185..f87f817 100644 --- a/src/snek/static/polyfills/Promise.withResolvers.js +++ b/src/snek/static/polyfills/Promise.withResolvers.js @@ -1,3 +1,5 @@ +// retoor + Promise.withResolvers = Promise.withResolvers || function() { let resolve, reject; let promise = new Promise((res, rej) => { @@ -5,4 +7,4 @@ Promise.withResolvers = Promise.withResolvers || function() { reject = rej; }); return { promise, resolve, reject }; -} \ No newline at end of file +} diff --git a/src/snek/static/push.js b/src/snek/static/push.js index 1652817..709cf03 100644 --- a/src/snek/static/push.js +++ b/src/snek/static/push.js @@ -1,3 +1,5 @@ +// retoor + export const registerServiceWorker = async (silent = false) => { try { const serviceWorkerRegistration = await navigator.serviceWorker @@ -36,7 +38,9 @@ export const registerServiceWorker = async (silent = false) => { } catch (error) { console.error("Error registering service worker:", error); if (!silent) { - alert("Registering push notifications failed. Please check your browser settings and try again.\n\n" + error); + alert("Registering push notifications failed. Please check your browser settings and try again. + +" + error); } } } diff --git a/src/snek/static/schedule.js b/src/snek/static/schedule.js index 19ab82c..51cd0c2 100644 --- a/src/snek/static/schedule.js +++ b/src/snek/static/schedule.js @@ -1,3 +1,5 @@ +// retoor + // Written by retoor@molodetz.nl // This JavaScript class provides functionality to schedule repeated execution of a function or delay its execution using specified intervals and timeouts. diff --git a/src/snek/static/service-worker.js b/src/snek/static/service-worker.js index 23b6369..c77f3a3 100644 --- a/src/snek/static/service-worker.js +++ b/src/snek/static/service-worker.js @@ -1,3 +1,5 @@ +// retoor + function isClientOpen(url) { return clients.matchAll().then((matchedClients) => { return matchedClients.some((matchedClient) => { diff --git a/src/snek/static/socket.js b/src/snek/static/socket.js index 363dabd..48a3295 100644 --- a/src/snek/static/socket.js +++ b/src/snek/static/socket.js @@ -1,6 +1,9 @@ // retoor import { EventHandler } from "./event-handler.js"; +import { loggerFactory } from "./logger.js"; + +const log = loggerFactory.getLogger("Socket"); function createPromiseWithResolvers() { let resolve, reject; @@ -45,25 +48,30 @@ export class Socket extends EventHandler { try { this.url = new URL("/rpc.ws", window.location.origin); this.url.protocol = this.url.protocol.replace("http", "ws"); + log.info("Socket initializing", { url: this.url.toString() }); this.connect(); } catch (e) { - console.error("Socket initialization failed:", e); + log.error("Socket initialization failed", e); } } connect() { if (this._isDestroyed) { + log.warn("Connect called on destroyed socket"); return Promise.reject(new Error("Socket destroyed")); } if (this.ws && (this.isConnected || this.isConnecting)) { + log.debug("Already connected or connecting"); return this.connection ? this.connection.promise : Promise.resolve(this); } try { + log.info("Connecting to WebSocket server"); this._cleanup(); if (!this.connection || this.connection.resolved) { this.connection = createPromiseWithResolvers(); } if (!this.url) { + log.error("URL not initialized"); this.connection.reject(new Error("URL not initialized")); return this.connection.promise; } @@ -71,30 +79,31 @@ export class Socket extends EventHandler { this.ws.addEventListener("open", () => { try { this._reconnectAttempts = 0; + log.info("WebSocket connection established"); if (this.connection && !this.connection.resolved) { this.connection.resolved = true; this.connection.resolve(this); } this.emit("connected"); } catch (e) { - console.error("Open handler error:", e); + log.error("Open handler error", e); } }); this.ws.addEventListener("close", (event) => { try { const reason = event.reason || "Connection closed"; - console.log("Connection closed:", reason); + log.info("Connection closed", { reason, code: event.code }); this._handleDisconnect(); } catch (e) { - console.error("Close handler error:", e); + log.error("Close handler error", e); } }); this.ws.addEventListener("error", (e) => { try { - console.error("Connection error:", e); + log.error("Connection error", e); this._handleDisconnect(); } catch (ex) { - console.error("Error handler error:", ex); + log.error("Error handler error", ex); } }); this.ws.addEventListener("message", (e) => { @@ -102,32 +111,34 @@ export class Socket extends EventHandler { }); return this.connection.promise; } catch (e) { - console.error("Connect failed:", e); + log.error("Connect failed", e); return Promise.reject(e); } } _handleMessage(e) { if (!e || !e.data) { + log.debug("Empty message received"); return; } try { if (e.data instanceof Blob || e.data instanceof ArrayBuffer) { - console.warn("Binary data not supported"); + log.warn("Binary data not supported"); return; } let data; try { data = JSON.parse(e.data); } catch (parseError) { - console.error("Failed to parse message:", parseError); + log.error("Failed to parse message", { error: parseError, raw: e.data.substring(0, 200) }); return; } + log.debug("Message received", { callId: data?.callId, event: data?.event, channel_uid: data?.channel_uid }); if (data) { this.onData(data); } } catch (error) { - console.error("Message handling error:", error); + log.error("Message handling error", error); } } @@ -312,29 +323,35 @@ export class Socket extends EventHandler { async sendJson(data) { if (this._isDestroyed) { + log.error("sendJson called on destroyed socket"); throw new Error("Socket destroyed"); } if (!data) { + log.error("sendJson called with no data"); throw new Error("No data to send"); } try { await this.connect(); if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + log.error("sendJson: WebSocket not open", { readyState: this.ws?.readyState }); throw new Error("WebSocket not open"); } const jsonStr = JSON.stringify(data); + log.debug("Sending JSON", { method: data.method, callId: data.callId, argsLength: data.args?.length }); this.ws.send(jsonStr); } catch (e) { - console.error("sendJson error:", e); + log.error("sendJson error", e); throw e; } } async call(method, ...args) { if (this._isDestroyed) { + log.error("RPC call on destroyed socket", { method }); return Promise.reject(new Error("Socket destroyed")); } if (!method || typeof method !== "string") { + log.error("Invalid RPC method name", { method }); return Promise.reject(new Error("Invalid method name")); } const callId = this.generateCallId(); @@ -343,9 +360,12 @@ export class Socket extends EventHandler { method, args: args || [], }; + log.info("RPC call initiated", { method, callId, argsCount: args.length }); + const startTime = Date.now(); return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { try { + log.error("RPC call timeout", { method, callId, elapsed: Date.now() - startTime }); this._pendingCalls.delete(callId); this.removeEventListener(callId, handler); reject(new Error(`RPC call timeout: ${method}`)); @@ -356,26 +376,32 @@ export class Socket extends EventHandler { this._pendingCalls.set(callId, timeoutId); const handler = (response) => { try { + const elapsed = Date.now() - startTime; clearTimeout(timeoutId); this._pendingCalls.delete(callId); if (response && !response.success && response.error) { + log.error("RPC call failed", { method, callId, elapsed, error: response.error }); reject(new Error(response.error)); } else { + log.debug("RPC call completed", { method, callId, elapsed, success: true }); resolve(response ? response.data : null); } } catch (e) { + log.error("RPC handler error", { method, callId, error: e }); reject(e); } }; try { this.addEventListener(callId, handler, { once: true }); this.sendJson(callData).catch((e) => { + log.error("RPC sendJson failed", { method, callId, error: e }); clearTimeout(timeoutId); this._pendingCalls.delete(callId); this.removeEventListener(callId, handler); reject(e); }); } catch (e) { + log.error("RPC call setup error", { method, callId, error: e }); clearTimeout(timeoutId); this._pendingCalls.delete(callId); reject(e); diff --git a/src/snek/static/stt.js b/src/snek/static/stt.js index 1c6867c..8ad2372 100644 --- a/src/snek/static/stt.js +++ b/src/snek/static/stt.js @@ -1,3 +1,5 @@ +// retoor + class STTButton extends HTMLElement { static get observedAttributes() { return ['target']; } @@ -92,9 +94,12 @@ class STTButton extends HTMLElement { committed += punctuated + ' '; if (this.targetEl) { this.targetEl.focus(); - punctuated = punctuated.replace(/\./g, ".\n") - .replace(/\?/g, "?\n") - .replace(/\!/g, "!\n"); + punctuated = punctuated.replace(/\./g, ". +") + .replace(/\?/g, "? +") + .replace(/\!/g, "! +"); this.targetEl.value = punctuated; // punctuated; this.simulateTypingWithEvents(this.targetEl, ' ', 0).then(() => { @@ -178,4 +183,3 @@ class STTButton extends HTMLElement { } customElements.define('stt-button', STTButton); - diff --git a/src/snek/static/tts.js b/src/snek/static/tts.js index dbe5e5b..abb86c1 100644 --- a/src/snek/static/tts.js +++ b/src/snek/static/tts.js @@ -1,3 +1,5 @@ +// retoor + class SnekSpeaker extends HTMLElement { _enabled = false @@ -66,4 +68,3 @@ class SnekSpeaker extends HTMLElement { // Define the element customElements.define('snek-speaker', SnekSpeaker); - diff --git a/src/snek/static/upload-button.js b/src/snek/static/upload-button.js index a789c96..da687fe 100644 --- a/src/snek/static/upload-button.js +++ b/src/snek/static/upload-button.js @@ -1,3 +1,5 @@ +// retoor + // Written by retoor@molodetz.nl // This class defines a custom HTML element for an upload button with integrated file upload functionality using XMLHttpRequest. diff --git a/src/snek/static/user-list.js b/src/snek/static/user-list.js index a8a77e6..a5692cb 100644 --- a/src/snek/static/user-list.js +++ b/src/snek/static/user-list.js @@ -1,3 +1,5 @@ +// retoor + class UserList extends HTMLElement { constructor() { super(); diff --git a/src/snek/sync.py b/src/snek/sync.py index c1ff46d..44813bf 100644 --- a/src/snek/sync.py +++ b/src/snek/sync.py @@ -1,22 +1,35 @@ +# retoor + + class DatasetWebSocketView: def __init__(self): self.ws = None - self.db = dataset.connect('sqlite:///snek.db') - setattr(self, "db", self.get) - setattr(self, "db", self.set) - super() - + def format_result(self, result): - + pass + async def send_str(self, msg): + pass + def get(self, key): + pass + def set(self, key, value): - class BroadCastSocketView: + pass + + +class BroadCastSocketView: def __init__(self): self.ws = None - + def format_result(self, result): - + pass + async def send_str(self, msg): + pass + def get(self, key): + pass + def set(self, key, value): + pass diff --git a/src/snek/system/__init__.py b/src/snek/system/__init__.py index e69de29..5f25e13 100644 --- a/src/snek/system/__init__.py +++ b/src/snek/system/__init__.py @@ -0,0 +1,3 @@ +# retoor + + diff --git a/src/snek/system/api.py b/src/snek/system/api.py index e69de29..5f25e13 100644 --- a/src/snek/system/api.py +++ b/src/snek/system/api.py @@ -0,0 +1,3 @@ +# retoor + + diff --git a/src/snek/system/cache.py b/src/snek/system/cache.py index 5e3f7ef..522351f 100644 --- a/src/snek/system/cache.py +++ b/src/snek/system/cache.py @@ -1,3 +1,5 @@ +# retoor + import asyncio import functools import json @@ -123,10 +125,9 @@ class Cache: return cpy def json_default(self, value): - """JSON serializer fallback for objects that are not directly serializable.""" try: return json.dumps(value.__dict__, default=str) - except: + except (TypeError, AttributeError): return str(value) async def create_cache_key(self, args, kwargs): @@ -161,4 +162,3 @@ class Cache: await self.delete(cache_key) return await func(*args, **kwargs) return wrapper - diff --git a/src/snek/system/debug.py b/src/snek/system/debug.py index 194512c..7cc9d8e 100644 --- a/src/snek/system/debug.py +++ b/src/snek/system/debug.py @@ -1,3 +1,5 @@ +# retoor + import asyncio import functools import inspect diff --git a/src/snek/system/docker.py b/src/snek/system/docker.py index c72620b..d4a8a20 100644 --- a/src/snek/system/docker.py +++ b/src/snek/system/docker.py @@ -1,3 +1,5 @@ +# retoor + import copy import json import yaml @@ -230,4 +232,3 @@ class ComposeFileManager: self.running_instances[name] = proc await self._create_readers(name) return True - diff --git a/src/snek/system/exceptions.py b/src/snek/system/exceptions.py index bf7a1f6..e7edae5 100644 --- a/src/snek/system/exceptions.py +++ b/src/snek/system/exceptions.py @@ -1,3 +1,5 @@ +# retoor + class SnekException(Exception): def __init__(self, message: str, details: dict = None): super().__init__(message) diff --git a/src/snek/system/form.py b/src/snek/system/form.py index f4cf2d3..accea4f 100644 --- a/src/snek/system/form.py +++ b/src/snek/system/form.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl # This code defines a framework for handling HTML elements as Python objects, including specific classes for HTML, form input, and form button elements. It offers methods to convert these elements to JSON, manipulate them, and validate form data. diff --git a/src/snek/system/http.py b/src/snek/system/http.py index a1e87a4..c7f6e25 100644 --- a/src/snek/system/http.py +++ b/src/snek/system/http.py @@ -1,28 +1,4 @@ -# Written by retoor@molodetz.nl - -# This script enables downloading, processing, and caching web content, including taking website screenshots and repairing links in HTML content. - -# Imports used: aiohttp, aiohttp.web for creating web servers and handling async requests; app.cache for caching utilities; BeautifulSoup from bs4 for HTML parsing; imgkit for creating screenshots. - -# The MIT License (MIT) -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - +# retoor import asyncio import pathlib @@ -39,7 +15,7 @@ from bs4 import BeautifulSoup async def crc32(data): try: data = data.encode() - except: + except AttributeError: pass return "crc32" + str(zlib.crc32(data)) @@ -90,7 +66,7 @@ async def is_html_content(content: bytes): return False try: content = content.decode(errors="ignore") - except: + except AttributeError: pass marks = [" + import asyncio import typing -import time + from snek.system.model import BaseModel +DEFAULT_LIMIT = 30 + class BaseMapper: model_class: BaseModel = None default_limit: int = DEFAULT_LIMIT table_name: str = None - semaphore = asyncio.Semaphore(1) - + def __init__(self, app): self.app = app + self.semaphore = asyncio.Semaphore(1) @property def db(self): @@ -25,14 +28,13 @@ class BaseMapper: async def run_in_executor(self, func, *args, **kwargs): use_semaphore = kwargs.pop("use_semaphore", False) - start_time = time.time() - - def _execute(): + + def _execute(): result = func(*args, **kwargs) if use_semaphore: self.db.commit() - return result - + return result + async with self.semaphore: return await asyncio.to_thread(_execute) @@ -48,6 +50,9 @@ class BaseMapper: kwargs["uid"] = uid if not kwargs.get("deleted_at"): kwargs["deleted_at"] = None + for key, val in list(kwargs.items()): + if hasattr(val, 'value') and hasattr(val, '_value'): + kwargs[key] = val.value record = await self.run_in_executor(self.table.find_one, **kwargs) if not record: return None @@ -74,6 +79,9 @@ class BaseMapper: kwargs["_limit"] = self.default_limit if not kwargs.get("deleted_at"): kwargs["deleted_at"] = None + for key, val in list(kwargs.items()): + if hasattr(val, 'value') and hasattr(val, '_value'): + kwargs[key] = val.value for record in await self.run_in_executor(self.table.find, **kwargs): model = await self.new() for key, value in record.items(): diff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py index 3160ab7..471781b 100644 --- a/src/snek/system/markdown.py +++ b/src/snek/system/markdown.py @@ -1,4 +1,5 @@ -# Original source: https://brandonjay.dev/posts/2021/render-markdown-html-in-python-with-jinja2 +# retoor + import re from types import SimpleNamespace @@ -48,7 +49,7 @@ class MarkdownRenderer(HTMLRenderer): def get_lexer(self, lang, default="bash"): try: return get_lexer_by_name(lang, stripall=True) - except: + except Exception: return get_lexer_by_name(default, stripall=True) @time_cache(timeout=60 * 60) diff --git a/src/snek/system/middleware.py b/src/snek/system/middleware.py index 61cd97e..1ed6ef0 100644 --- a/src/snek/system/middleware.py +++ b/src/snek/system/middleware.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl # This code provides middleware functions for an aiohttp server to manage and modify CSP, CORS, and authentication headers. diff --git a/src/snek/system/model.py b/src/snek/system/model.py index a4861a8..442dfcc 100644 --- a/src/snek/system/model.py +++ b/src/snek/system/model.py @@ -1,8 +1,4 @@ -# Written by retoor@molodetz.nl - -# The script defines a flexible validation and field management system for models, with capabilities for setting attributes, validation, error handling, and JSON conversion. It includes classes for managing various field types with specific properties such as UUID, timestamps for creation and updates, and custom validation rules. - -# This script utilizes external Python libraries such as 're' for regex operations, 'uuid' for generating unique identifiers, and 'json' for data interchange. The 'datetime' and 'timezone' modules from the Python standard library are used for date and time operations. 'OrderedDict' from 'collections' provides enhanced dictionary capabilities, and 'copy' allows deep copying of objects. +# retoor # MIT License # @@ -59,6 +55,7 @@ def validate_attrs( regex=regex, **kwargs, )(func) + return decorator class Validator: @@ -363,13 +360,6 @@ class BaseModel: return model_data -class FormElement(ModelField): - - def __init__(self, place_holder=None, *args, **kwargs): - super().__init__(*args, **kwargs) - self.place_holder = place_holder - - class FormElement(ModelField): def __init__(self, place_holder=None, *args, **kwargs): diff --git a/src/snek/system/object.py b/src/snek/system/object.py index f91ec42..e93dc45 100644 --- a/src/snek/system/object.py +++ b/src/snek/system/object.py @@ -1,3 +1,5 @@ +# retoor + class Object: def __init__(self, *args, **kwargs): diff --git a/src/snek/system/profiler.py b/src/snek/system/profiler.py index e0e5542..35ccfe6 100644 --- a/src/snek/system/profiler.py +++ b/src/snek/system/profiler.py @@ -1,3 +1,5 @@ +# retoor + import cProfile import pstats import sys diff --git a/src/snek/system/security.py b/src/snek/system/security.py index a5b9302..45f230f 100644 --- a/src/snek/system/security.py +++ b/src/snek/system/security.py @@ -1,3 +1,5 @@ +# retoor + import hashlib import uuid diff --git a/src/snek/system/service.py b/src/snek/system/service.py index 01570d8..5ba7210 100644 --- a/src/snek/system/service.py +++ b/src/snek/system/service.py @@ -1,5 +1,6 @@ +# retoor + from snek.mapper import get_mapper -from snek.model.user import UserModel from snek.system.mapper import BaseMapper @@ -57,12 +58,11 @@ class BaseService: return result 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) return True errors = await model.errors - raise Exception(f"Couldn't save model. Errors: f{errors}") + raise Exception(f"Couldn't save model. Errors: {errors}") async def find(self, **kwargs): if "_limit" not in kwargs or int(kwargs.get("_limit")) > 30: diff --git a/src/snek/system/template.py b/src/snek/system/template.py index 5320af9..4bde3b1 100644 --- a/src/snek/system/template.py +++ b/src/snek/system/template.py @@ -1,3 +1,5 @@ +# retoor + import mimetypes import re from types import SimpleNamespace @@ -259,7 +261,7 @@ def embed_media(text): def linkify_https(text): if "https://" not in text: return text - url_pattern = r'(?()]+(?()]+(? + from aiohttp import web from snek.system.markdown import render_markdown @@ -63,9 +65,6 @@ class BaseFormView(BaseView): post = await self.request.json() form.set_user_data(post["form"]) result = await form.to_json() - if post.get("action") == "validate": - # Pass - pass if post.get("action") == "submit" and result["is_valid"]: result = await self.submit(form) return await self.json_response(result) diff --git a/src/snek/view/__init__.py b/src/snek/view/__init__.py index e69de29..5f25e13 100644 --- a/src/snek/view/__init__.py +++ b/src/snek/view/__init__.py @@ -0,0 +1,3 @@ +# retoor + + diff --git a/src/snek/view/about.py b/src/snek/view/about.py index aba57ae..ba14601 100644 --- a/src/snek/view/about.py +++ b/src/snek/view/about.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl # This source code defines two classes, `AboutHTMLView` and `AboutMDView`, both inheriting from `BaseView`. They asynchronously return rendered templates for HTML and Markdown respectively. diff --git a/src/snek/view/avatar.py b/src/snek/view/avatar.py index 1a9c2d9..d84e18c 100644 --- a/src/snek/view/avatar.py +++ b/src/snek/view/avatar.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl # This code defines a WebView class that inherits from BaseView and includes a method for rendering a web template, requiring login access for its usage. diff --git a/src/snek/view/avatar_animal_view.py b/src/snek/view/avatar_animal_view.py index ebcca95..506e58b 100644 --- a/src/snek/view/avatar_animal_view.py +++ b/src/snek/view/avatar_animal_view.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl # This code defines a WebView class that inherits from BaseView and includes a method for rendering a web template, requiring login access for its usage. diff --git a/src/snek/view/channel.py b/src/snek/view/channel.py index 2fcdfc1..6010c1d 100644 --- a/src/snek/view/channel.py +++ b/src/snek/view/channel.py @@ -1,3 +1,5 @@ +# retoor + import asyncio import mimetypes import pathlib diff --git a/src/snek/view/docs.py b/src/snek/view/docs.py index bb63413..e877ad0 100644 --- a/src/snek/view/docs.py +++ b/src/snek/view/docs.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl # This code defines two classes, DocsHTMLView and DocsMDView, which are intended to asynchronously render HTML and Markdown templates respectively. Both classes inherit from the BaseView class. diff --git a/src/snek/view/drive.py b/src/snek/view/drive.py index 733c441..5ab5dae 100644 --- a/src/snek/view/drive.py +++ b/src/snek/view/drive.py @@ -1,3 +1,5 @@ +# retoor + import mimetypes import os import urllib.parse diff --git a/src/snek/view/forum.py b/src/snek/view/forum.py index 9df6c9b..0e70d55 100644 --- a/src/snek/view/forum.py +++ b/src/snek/view/forum.py @@ -1,3 +1,5 @@ +# retoor + # views/forum.py from snek.system.view import BaseView from aiohttp import web diff --git a/src/snek/view/git_docs.py b/src/snek/view/git_docs.py index ec8004e..de73874 100644 --- a/src/snek/view/git_docs.py +++ b/src/snek/view/git_docs.py @@ -1,3 +1,5 @@ +# retoor + import logging from snek.system.view import BaseView diff --git a/src/snek/view/index.py b/src/snek/view/index.py index 2f44443..ad4bd12 100644 --- a/src/snek/view/index.py +++ b/src/snek/view/index.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl diff --git a/src/snek/view/login.py b/src/snek/view/login.py index fe8cf4d..2dc996d 100644 --- a/src/snek/view/login.py +++ b/src/snek/view/login.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl # This source code defines a LoginView class that inherits from BaseFormView and handles user authentication. It checks if a user is logged in, provides a JSON response or renders a login HTML template as needed, and processes form submissions to authenticate users. diff --git a/src/snek/view/login_form.py b/src/snek/view/login_form.py index acf7c75..241d997 100644 --- a/src/snek/view/login_form.py +++ b/src/snek/view/login_form.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl # This code defines an asynchronous view for handling a login form. It checks if the form is valid, sets session variables for a logged-in user, and provides a redirect URL if successful. diff --git a/src/snek/view/logout.py b/src/snek/view/logout.py index 42016d8..43fd87d 100644 --- a/src/snek/view/logout.py +++ b/src/snek/view/logout.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl diff --git a/src/snek/view/new.py b/src/snek/view/new.py index 87ed3cf..2cee2b8 100644 --- a/src/snek/view/new.py +++ b/src/snek/view/new.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.view import BaseView @@ -5,4 +7,4 @@ class NewView(BaseView): login_required = True async def get(self): - return await self.render_template("new.html") \ No newline at end of file + return await self.render_template("new.html") diff --git a/src/snek/view/profile_page.py b/src/snek/view/profile_page.py index 8e12c22..c2e7c99 100644 --- a/src/snek/view/profile_page.py +++ b/src/snek/view/profile_page.py @@ -1,3 +1,5 @@ +# retoor + import logging from aiohttp import web from snek.system.view import BaseView diff --git a/src/snek/view/push.py b/src/snek/view/push.py index 169ef63..ada98e1 100644 --- a/src/snek/view/push.py +++ b/src/snek/view/push.py @@ -1,3 +1,5 @@ +# retoor + import base64 import json diff --git a/src/snek/view/register.py b/src/snek/view/register.py index 96eed8a..d561454 100644 --- a/src/snek/view/register.py +++ b/src/snek/view/register.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl # This module defines a web view for user registration. It handles GET requests and form submissions for the registration process. diff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py index 850e672..a1839e6 100644 --- a/src/snek/view/register_form.py +++ b/src/snek/view/register_form.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl # This code defines a `RegisterFormView` class that handles the user registration process by using a form object, a view parent class, and asynchronously submitting the form data to register a user. It then stores the user's session details and provides a redirect URL to a specific page. diff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py index ce52abd..a105bdc 100644 --- a/src/snek/view/rpc.py +++ b/src/snek/view/rpc.py @@ -19,8 +19,11 @@ def safe_get(obj, key, default=None): try: if isinstance(obj, dict): return obj.get(key, default) + if hasattr(obj, "fields") and hasattr(obj, "__getitem__"): + val = obj[key] + return val if val is not None else default return getattr(obj, key, default) - except Exception: + except (KeyError, TypeError, AttributeError): return default @@ -97,17 +100,21 @@ class RPCView(BaseView): return await self.services.db.update(self.user_uid, table_name, record) async def set_typing(self, channel_uid, color=None): + logger.debug(f"set_typing: called channel_uid={channel_uid}, user_uid={self.user_uid}, color={color}") self._require_login() self._require_services() if not channel_uid or not isinstance(channel_uid, str): + logger.warning(f"set_typing: invalid channel_uid: {channel_uid}") return False try: user = await self.services.user.get(uid=self.user_uid) if not user: + logger.warning(f"set_typing: user not found: {self.user_uid}") return False if not color: color = safe_get(user, "color", "#000000") - return await self.services.socket.broadcast( + logger.debug(f"set_typing: broadcasting typing indicator to channel={channel_uid}, user={safe_get(user, 'username')}") + result = await self.services.socket.broadcast( channel_uid, { "channel_uid": channel_uid, @@ -122,6 +129,8 @@ class RPCView(BaseView): }, }, ) + logger.debug(f"set_typing: broadcast result={result}") + return result except Exception as ex: logger.warning(f"Failed to set typing: {safe_str(ex)}") return False @@ -250,7 +259,7 @@ class RPCView(BaseView): deleted_at=None, is_banned=False, ): - if subscription and safe_get(subscription, "channel_uid"): + if subscription and subscription["channel_uid"]: await self.services.socket.subscribe( self.ws, subscription["channel_uid"], @@ -359,7 +368,7 @@ class RPCView(BaseView): if not subscription: continue try: - channel_uid = safe_get(subscription, "channel_uid") + channel_uid = subscription["channel_uid"] if not channel_uid: continue channel = await self.services.channel.get(uid=channel_uid) @@ -374,12 +383,12 @@ class RPCView(BaseView): except Exception: pass channels.append({ - "name": safe_get(subscription, "label", ""), + "name": subscription["label"] or "", "uid": channel_uid, - "tag": safe_get(channel, "tag", ""), - "new_count": safe_get(subscription, "new_count", 0), - "is_moderator": safe_get(subscription, "is_moderator", False), - "is_read_only": safe_get(subscription, "is_read_only", False), + "tag": channel["tag"] or "", + "new_count": subscription["new_count"] or 0, + "is_moderator": subscription["is_moderator"] or False, + "is_read_only": subscription["is_read_only"] or False, "color": color, }) except Exception as ex: @@ -532,35 +541,48 @@ class RPCView(BaseView): logger.warning(f"Failed to queue finalize: {safe_str(ex)}") async def send_message(self, channel_uid, message, is_final=True): + logger.debug(f"send_message called: channel_uid={channel_uid}, message_len={len(str(message)) if message else 0}, is_final={is_final}, user_uid={self.user_uid}") self._require_login() self._require_services() if not channel_uid or not isinstance(channel_uid, str): + logger.warning(f"send_message: invalid channel_uid: {channel_uid}") return None if message is None: + logger.debug("send_message: message is None") return None try: message_text = safe_str(message).strip() + logger.debug(f"send_message: processing message text, length={len(message_text)}, is_final={is_final}") if not is_final: + logger.debug(f"send_message: checking for existing non-final message in channel={channel_uid}") check_message = await self.services.channel_message.get( channel_uid=channel_uid, user_uid=self.user_uid, is_final=False, deleted_at=None ) if check_message: - await self._queue_finalize_message(safe_get(check_message, "uid")) - return await self.update_message_text(safe_get(check_message, "uid"), message_text) + msg_uid = safe_get(check_message, "uid") + logger.debug(f"send_message: found existing non-final message uid={msg_uid}, updating text") + await self._queue_finalize_message(msg_uid) + return await self.update_message_text(msg_uid, message_text) if not message_text: + logger.debug("send_message: empty message text after strip") return None + logger.info(f"send_message: sending new message to channel={channel_uid}, is_final={is_final}") result = await self.services.chat.send( self.user_uid, channel_uid, message_text, is_final ) if not result: + logger.warning(f"send_message: chat.send returned None for channel={channel_uid}") return None - if not safe_get(result, "is_final", True): - await self._queue_finalize_message(safe_get(result, "uid")) + result_uid = safe_get(result, "uid") + result_is_final = safe_get(result, "is_final", True) + logger.info(f"send_message: message sent successfully, uid={result_uid}, is_final={result_is_final}") + if not result_is_final: + await self._queue_finalize_message(result_uid) elif self._finalize_task: self._finalize_task.cancel() - return safe_get(result, "uid") + return result_uid except Exception as ex: - logger.warning(f"Failed to send message: {safe_str(ex)}") + logger.warning(f"Failed to send message: {safe_str(ex)}", exc_info=True) return None async def start_container(self, channel_uid): @@ -731,44 +753,57 @@ class RPCView(BaseView): async def __call__(self, data): call_id = None + start_time = time.time() try: if self._is_closed: + logger.debug("RPC call on closed connection") return if not data or not isinstance(data, dict): logger.warning("Invalid RPC data received") return call_id = data.get("callId") method_name = data.get("method") + args = data.get("args", []) + logger.debug(f"RPC call received: method={method_name}, callId={call_id}, args_count={len(args) if args else 0}, user_uid={self.user_uid}") if not method_name or not isinstance(method_name, str): + logger.warning(f"Invalid method name: {method_name}") await self._send_json({"callId": call_id, "success": False, "data": "Invalid method"}) return if method_name.startswith("_"): + logger.warning(f"Attempted to call private method: {method_name}") await self._send_json({"callId": call_id, "success": False, "data": "Not allowed"}) return - args = data.get("args") if args is None: args = [] elif not isinstance(args, (list, tuple)): args = [args] safe_method_name = method_name.replace(".", "_") if not hasattr(self, safe_method_name): + logger.warning(f"Method not found: {method_name}") await self._send_json({"callId": call_id, "success": False, "data": "Method not found"}) return method = getattr(self, safe_method_name, None) if not method or not callable(method): + logger.warning(f"Method not callable: {method_name}") await self._send_json({"callId": call_id, "success": False, "data": "Method not callable"}) return success = True result = None try: + logger.debug(f"Executing RPC method: {method_name}") result = await method(*args) + elapsed = (time.time() - start_time) * 1000 + logger.info(f"RPC method completed: method={method_name}, callId={call_id}, elapsed={elapsed:.2f}ms, success=True") except (PermissionError, ValueError) as ex: result = {"error": safe_str(ex)} success = False + elapsed = (time.time() - start_time) * 1000 + logger.warning(f"RPC method permission/value error: method={method_name}, callId={call_id}, elapsed={elapsed:.2f}ms, error={safe_str(ex)}") except Exception as ex: result = {"error": "Internal error"} success = False - logger.exception(f"RPC method {method_name} failed: {safe_str(ex)}") + elapsed = (time.time() - start_time) * 1000 + logger.exception(f"RPC method failed: method={method_name}, callId={call_id}, elapsed={elapsed:.2f}ms, error={safe_str(ex)}") if result != "noresponse": await self._send_json({"callId": call_id, "success": success, "data": result}) except Exception as ex: @@ -983,7 +1018,7 @@ class RPCView(BaseView): try: app = getattr(self.request, "app", None) uptime_seconds = getattr(app, "uptime_seconds", 999) if app else 999 - if not scheduled and uptime_seconds < 240 and user_uid: + if not scheduled and uptime_seconds < 15 and user_uid: jitter = random.uniform(0, 3) asyncio.create_task(schedule( user_uid, jitter, diff --git a/src/snek/view/search_user.py b/src/snek/view/search_user.py index 1f09a26..0598fb1 100644 --- a/src/snek/view/search_user.py +++ b/src/snek/view/search_user.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl # This code implements a web view feature for searching users. It handles GET requests to retrieve user data based on a search query and also processes form submissions. diff --git a/src/snek/view/settings/containers.py b/src/snek/view/settings/containers.py index b6c4bfc..4d82558 100644 --- a/src/snek/view/settings/containers.py +++ b/src/snek/view/settings/containers.py @@ -1,3 +1,5 @@ +# retoor + from aiohttp import web from snek.system.view import BaseFormView diff --git a/src/snek/view/settings/index.py b/src/snek/view/settings/index.py index 418ef3d..ddcc9c1 100644 --- a/src/snek/view/settings/index.py +++ b/src/snek/view/settings/index.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.view import BaseView diff --git a/src/snek/view/settings/profile.py b/src/snek/view/settings/profile.py index 164c526..5349950 100644 --- a/src/snek/view/settings/profile.py +++ b/src/snek/view/settings/profile.py @@ -1,3 +1,5 @@ +# retoor + from aiohttp import web from snek.form.settings.profile import SettingsProfileForm diff --git a/src/snek/view/settings/profile_pages.py b/src/snek/view/settings/profile_pages.py index b6f67c4..0144440 100644 --- a/src/snek/view/settings/profile_pages.py +++ b/src/snek/view/settings/profile_pages.py @@ -1,3 +1,5 @@ +# retoor + import logging from aiohttp import web from snek.system.view import BaseView diff --git a/src/snek/view/settings/repositories.py b/src/snek/view/settings/repositories.py index fab44cc..4a9fb80 100644 --- a/src/snek/view/settings/repositories.py +++ b/src/snek/view/settings/repositories.py @@ -1,3 +1,5 @@ +# retoor + from aiohttp import web from snek.system.view import BaseFormView diff --git a/src/snek/view/stats.py b/src/snek/view/stats.py index 24b15f4..f10cac2 100644 --- a/src/snek/view/stats.py +++ b/src/snek/view/stats.py @@ -1,3 +1,5 @@ +# retoor + import json from aiohttp import web diff --git a/src/snek/view/status.py b/src/snek/view/status.py index fe37902..47348f8 100644 --- a/src/snek/view/status.py +++ b/src/snek/view/status.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl # This code defines an async class-based view called StatusView for handling HTTP GET requests. It fetches user details and their associated channel memberships from a database and returns a JSON response with user information if the user is logged in. diff --git a/src/snek/view/terminal.py b/src/snek/view/terminal.py index d3af9b0..839de5d 100644 --- a/src/snek/view/terminal.py +++ b/src/snek/view/terminal.py @@ -1,3 +1,5 @@ +# retoor + import pathlib import aiohttp diff --git a/src/snek/view/threads.py b/src/snek/view/threads.py index fdf272c..01681b4 100644 --- a/src/snek/view/threads.py +++ b/src/snek/view/threads.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.view import BaseView diff --git a/src/snek/view/user.py b/src/snek/view/user.py index 043b1c1..c6f84ae 100644 --- a/src/snek/view/user.py +++ b/src/snek/view/user.py @@ -1,3 +1,5 @@ +# retoor + from snek.system.view import BaseView diff --git a/src/snek/view/web.py b/src/snek/view/web.py index 77ff479..c914d04 100644 --- a/src/snek/view/web.py +++ b/src/snek/view/web.py @@ -1,3 +1,5 @@ +# retoor + # Written by retoor@molodetz.nl # This code defines a WebView class that inherits from BaseView and includes a method for rendering a web template, requiring login access for its usage. diff --git a/src/snek/webdav.py b/src/snek/webdav.py index eabb1e5..a1dcb44 100644 --- a/src/snek/webdav.py +++ b/src/snek/webdav.py @@ -1,3 +1,5 @@ +# retoor + import logging import pathlib import base64