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
This commit is contained in:
retoor 2025-12-18 23:48:50 +01:00
parent b710008dbe
commit 8bacd6aa3f
150 changed files with 751 additions and 308 deletions

View File

@ -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 ## 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. 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.

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "Snek" name = "Snek"
version = "1.8.0" version = "1.9.0"
readme = "README.md" readme = "README.md"
#license = { file = "LICENSE", content-type="text/markdown" } #license = { file = "LICENSE", content-type="text/markdown" }
description = "Snek Chat Application by Molodetz" description = "Snek Chat Application by Molodetz"

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
""" """
MIT License MIT License

View File

@ -1,18 +1,18 @@
# retoor <retoor@molodetz.nl>
import asyncio
import logging import logging
logging.basicConfig(level=logging.INFO)
import pathlib import pathlib
import shutil import shutil
import sqlite3 import sqlite3
import asyncio
import click import click
from aiohttp import web from aiohttp import web
from IPython import start_ipython
from snek.shell import Shell
from snek.app import Application from snek.app import Application
from snek.shell import Shell
logging.basicConfig(level=logging.INFO)
@click.group() @click.group()
@ -111,9 +111,16 @@ def init(db_path, source):
show_default=True, show_default=True,
help="Database path for the application", help="Database path for the application",
) )
def serve(port, host, db_path): @click.option(
# init(db_path) "--debug",
# asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 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) web.run_app(Application(db_path=f"sqlite:///{db_path}"), port=port, host=host)

View File

@ -1,21 +1,15 @@
# retoor <retoor@molodetz.nl>
import asyncio import asyncio
import logging import logging
import pathlib import pathlib
import ssl 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 time
import uuid import uuid
from concurrent.futures import ThreadPoolExecutor
from contextlib import asynccontextmanager
from datetime import datetime
from ipaddress import ip_address
import IP2Location import IP2Location
from aiohttp import web from aiohttp import web
@ -28,6 +22,7 @@ from aiohttp_session.cookie_storage import EncryptedCookieStorage
from app.app import Application as BaseApplication from app.app import Application as BaseApplication
from jinja2 import FileSystemLoader from jinja2 import FileSystemLoader
from snek import snode
from snek.mapper import get_mappers from snek.mapper import get_mappers
from snek.service import get_services from snek.service import get_services
from snek.sgit import GitApplication 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.docs import DocsHTMLView, DocsMDView
from snek.view.drive import DriveApiView, DriveView from snek.view.drive import DriveApiView, DriveView
from snek.view.channel import ChannelDriveApiView from snek.view.channel import ChannelDriveApiView
from snek.view.container import ContainerView
from snek.view.index import IndexView from snek.view.index import IndexView
from snek.view.login import LoginView from snek.view.login import LoginView
from snek.view.logout import LogoutView 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.repository import RepositoryView
from snek.view.rpc import RPCView from snek.view.rpc import RPCView
from snek.view.search_user import SearchUserView 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 ( from snek.view.settings.containers import (
ContainersCreateView, ContainersCreateView,
ContainersDeleteView, ContainersDeleteView,
@ -88,9 +84,12 @@ from snek.view.user import UserView
from snek.view.web import WebView from snek.view.web import WebView
from snek.webdav import WebdavApplication from snek.webdav import WebdavApplication
from snek.forum import setup_forum 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" SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
from snek.system.template import whitelist_attributes
@web.middleware @web.middleware
@ -103,21 +102,22 @@ async def session_middleware(request, handler):
@web.middleware @web.middleware
async def ip2location_middleware(request, handler): async def ip2location_middleware(request, handler):
response = await handler(request) response = await handler(request)
return response
ip = request.headers.get("X-Forwarded-For", request.remote) ip = request.headers.get("X-Forwarded-For", request.remote)
ipaddress = ip_address(ip) try:
if ipaddress.is_private: ipaddr = ip_address(ip)
if ipaddr.is_private:
return response
except ValueError:
return response return response
if not request.app.session.get("uid"): if not request.session.get("uid"):
return response 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: if not user:
return response return response
location = request.app.ip2location.get(ip) location = request.app.ip2location.get_all(ip)
user["city"]
if user["city"] != location.city: if user["city"] != location.city:
user["country_long"] = location.country user["country_long"] = location.country_long
user["country_short"] = locaion.country_short user["country_short"] = location.country_short
user["city"] = location.city user["city"] = location.city
user["region"] = location.region user["region"] = location.region
user["latitude"] = location.latitude user["latitude"] = location.latitude
@ -130,14 +130,12 @@ async def ip2location_middleware(request, handler):
@web.middleware @web.middleware
async def trailing_slash_middleware(request, handler): async def trailing_slash_middleware(request, handler):
if request.path and not request.path.endswith("/"): if request.path and not request.path.endswith("/"):
# Redirect to the same path with a trailing slash
raise web.HTTPFound(request.path + "/") raise web.HTTPFound(request.path + "/")
return await handler(request) return await handler(request)
class Application(BaseApplication): class Application(BaseApplication):
async def create_default_forum(self, app): 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)] forums = [f async for f in self.services.forum.find(is_active=True)]
if not forums: if not forums:
# Find admin user to be the creator # Find admin user to be the creator
@ -148,7 +146,6 @@ class Application(BaseApplication):
description="A place for general discussion.", description="A place for general discussion.",
created_by_uid=admin_user["uid"], created_by_uid=admin_user["uid"],
) )
print("Default forum 'General Discussion' created.")
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
middlewares = [ middlewares = [
@ -160,7 +157,7 @@ class Application(BaseApplication):
super().__init__( super().__init__(
middlewares=middlewares, middlewares=middlewares,
template_path=self.template_path, template_path=self.template_path,
client_max_size=1024 * 1024 * 1024 * 5 * args, client_max_size=1024 * 1024 * 1024 * 5,
**kwargs, **kwargs,
) )
session_setup(self, EncryptedCookieStorage(SESSION_KEY)) session_setup(self, EncryptedCookieStorage(SESSION_KEY))
@ -197,7 +194,6 @@ class Application(BaseApplication):
self.on_startup.append(self.prepare_database) self.on_startup.append(self.prepare_database)
self.on_startup.append(self.create_default_forum) self.on_startup.append(self.create_default_forum)
@property @property
def uptime_seconds(self): def uptime_seconds(self):
return (datetime.now() - self.time_start).total_seconds() return (datetime.now() - self.time_start).total_seconds()
@ -238,13 +234,8 @@ class Application(BaseApplication):
asyncio.create_task(app.ssh_server.wait_closed()) asyncio.create_task(app.ssh_server.wait_closed())
async def prepare_asyncio(self, app): async def prepare_asyncio(self, app):
# app.loop = asyncio.get_running_loop()
app.executor = ThreadPoolExecutor(max_workers=200) app.executor = ThreadPoolExecutor(max_workers=200)
app.loop.set_default_executor(self.executor) 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): async def create_task(self, task):
await self.tasks.put(task) await self.tasks.put(task)
@ -257,10 +248,9 @@ class Application(BaseApplication):
await task await task
self.tasks.task_done() self.tasks.task_done()
except Exception as ex: except Exception as ex:
print(ex) logger.error(f"Task runner error: {ex}")
self.db.commit() self.db.commit()
async def prepare_database(self, app): async def prepare_database(self, app):
self.db.query("PRAGMA journal_mode=WAL") self.db.query("PRAGMA journal_mode=WAL")
self.db.query("PRAGMA syncnorm=off") self.db.query("PRAGMA syncnorm=off")
@ -272,8 +262,8 @@ class Application(BaseApplication):
self.db["channel_member"].create_index(["channel_uid", "user_uid"]) self.db["channel_member"].create_index(["channel_uid", "user_uid"])
if not self.db["channel_message"].has_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"]) self.db["channel_message"].create_index(["channel_uid", "user_uid"])
except: except Exception as ex:
pass logger.warning(f"Index creation error: {ex}")
await self.services.drive.prepare_all() await self.services.drive.prepare_all()
self.loop.create_task(self.task_runner()) 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("/login.json", LoginView)
self.router.add_view("/register.html", RegisterView) self.router.add_view("/register.html", RegisterView)
self.router.add_view("/register.json", 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.html", SearchUserView)
self.router.add_view("/search-user.json", SearchUserView) self.router.add_view("/search-user.json", SearchUserView)
self.router.add_view("/avatar/{uid}.svg", AvatarView) 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("/http-photo", self.handle_http_photo)
self.router.add_get("/rpc.ws", RPCView) self.router.add_get("/rpc.ws", RPCView)
self.router.add_get("/c/{channel:.*}", ChannelView) 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( self.router.add_view(
"/channel/{channel_uid}/attachment.sock", ChannelAttachmentUploadView "/channel/{channel_uid}/attachment.sock", ChannelAttachmentUploadView
) )
self.router.add_view( self.router.add_view(
"/channel/attachment/{relative_url:.*}", ChannelAttachmentView "/channel/attachment/{relative_url:.*}", ChannelAttachmentView
)# )
self.router.add_view("/channel/{channel}.html", WebView) self.router.add_view("/channel/{channel}.html", WebView)
self.router.add_view("/threads.html", ThreadsView) self.router.add_view("/threads.html", ThreadsView)
self.router.add_view("/terminal.ws", TerminalSocketView) self.router.add_view("/terminal.ws", TerminalSocketView)
@ -376,10 +357,8 @@ class Application(BaseApplication):
self.add_subapp("/webdav", self.webdav) self.add_subapp("/webdav", self.webdav)
self.add_subapp("/git", self.git) self.add_subapp("/git", self.git)
setup_forum(self) setup_forum(self)
# self.router.add_get("/{file_path:.*}", self.static_handler)
async def handle_test(self, request): async def handle_test(self, request):
return await whitelist_attributes( return await whitelist_attributes(
self.render_template("test.html", request, context={"name": "retoor"}) 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"} body=path.read_bytes(), headers={"Content-Type": "image/png"}
) )
# @time_cache_async(60)
async def render_template(self, template, request, context=None): async def render_template(self, template, request, context=None):
start_time = time.perf_counter() start_time = time.perf_counter()
channels = [] channels = []
if not context: if not context:
context = {} context = {}
@ -441,10 +418,6 @@ class Application(BaseApplication):
request.session.get("uid") 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.original_loader = self.jinja2_env.loader
self.jinja2_env.loader = await self.get_user_template_loader( self.jinja2_env.loader = await self.get_user_template_loader(
@ -452,20 +425,18 @@ class Application(BaseApplication):
) )
try: try:
context["nonce"] = request['csp_nonce'] context["nonce"] = request["csp_nonce"]
except: except KeyError:
context['nonce'] = '?' context["nonce"] = "?"
rendered = await super().render_template(template, request, context) rendered = await super().render_template(template, request, context)
self.jinja2_env.loader = self.original_loader self.jinja2_env.loader = self.original_loader
end_time = time.perf_counter() end_time = time.perf_counter()
print(f"render_template took {end_time - start_time:.4f} seconds") logger.debug(f"render_template took {end_time - start_time:.4f} seconds")
# rendered.text = whitelist_attributes(rendered.text)
# rendered.headers['Content-Lenght'] = len(rendered.text)
return rendered return rendered
async def static_handler(self, request): async def static_handler(self, request):
file_name = request.match_info.get("filename", "") file_name = request.match_info.get("filename", "")
@ -506,13 +477,12 @@ class Application(BaseApplication):
@asynccontextmanager @asynccontextmanager
async def no_save(self): async def no_save(self):
stats = { stats = {"count": 0}
'count': 0
}
async def patched_save(*args, **kwargs): async def patched_save(*args, **kwargs):
await self.cache.set(args[0]["uid"], args[0]) await self.cache.set(args[0]["uid"], args[0])
stats['count'] = stats['count'] + 1 stats["count"] = stats["count"] + 1
print(f"save is ignored {stats['count']} times") logger.debug(f"save is ignored {stats['count']} times")
return args[0] return args[0]
save_original = self.services.channel_message.mapper.save save_original = self.services.channel_message.mapper.save
self.services.channel_message.mapper.save = patched_save self.services.channel_message.mapper.save = patched_save

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import asyncio import asyncio
import sys import sys

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import pathlib import pathlib
from aiohttp import web from aiohttp import web

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import asyncio import asyncio
from snek.app import app from snek.app import app

View File

@ -0,0 +1,3 @@
# retoor <retoor@molodetz.nl>

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
# forum_app.py # forum_app.py
import aiohttp.web import aiohttp.web
from snek.view.forum import ForumIndexView, ForumView, ForumWebSocketView from snek.view.forum import ForumIndexView, ForumView, ForumWebSocketView

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.app import app from snek.app import app
application = app application = app

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import functools import functools
from snek.mapper.channel import ChannelMapper from snek.mapper.channel import ChannelMapper

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.model.channel import ChannelModel from snek.model.channel import ChannelModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.model.channel_attachment import ChannelAttachmentModel from snek.model.channel_attachment import ChannelAttachmentModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.model.channel_member import ChannelMemberModel from snek.model.channel_member import ChannelMemberModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.model.channel_message import ChannelMessageModel from snek.model.channel_message import ChannelMessageModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.model.container import Container from snek.model.container import Container
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.model.drive import DriveModel from snek.model.drive import DriveModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.model.drive_item import DriveItemModel from snek.model.drive_item import DriveItemModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
# mapper/forum.py # mapper/forum.py
from snek.model.forum import ForumModel, ThreadModel, PostModel, PostLikeModel from snek.model.forum import ForumModel, ThreadModel, PostModel, PostLikeModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.model.notification import NotificationModel from snek.model.notification import NotificationModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import logging import logging
from snek.model.profile_page import ProfilePageModel from snek.model.profile_page import ProfilePageModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.model.push_registration import PushRegistrationModel from snek.model.push_registration import PushRegistrationModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.model.repository import RepositoryModel from snek.model.repository import RepositoryModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper

View File

@ -1,6 +1,12 @@
# retoor <retoor@molodetz.nl>
import logging
from snek.model.user import UserModel from snek.model.user import UserModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper
logger = logging.getLogger(__name__)
class UserMapper(BaseMapper): class UserMapper(BaseMapper):
table_name = "user" table_name = "user"
@ -16,5 +22,5 @@ class UserMapper(BaseMapper):
) )
] ]
except Exception as ex: except Exception as ex:
print(ex) logger.warning(f"Failed to get admin uids: {ex}")
return [] return []

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.model.user_property import UserPropertyModel from snek.model.user_property import UserPropertyModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import functools import functools
from snek.model.channel import ChannelModel from snek.model.channel import ChannelModel
@ -44,5 +46,3 @@ def get_models():
def get_model(name): def get_model(name):
return get_models()[name] return get_models()[name]

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.model.channel_message import ChannelMessageModel from snek.model.channel_message import ChannelMessageModel
from snek.system.model import BaseModel, ModelField 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", "SELECT uid FROM channel_message WHERE channel_uid=:channel_uid" + history_start_filter + " ORDER BY id DESC LIMIT 1",
{"channel_uid": self["uid"]}, {"channel_uid": self["uid"]},
): ):
return await self.app.services.channel_message.get(uid=model["uid"]) return await self.app.services.channel_message.get(uid=model["uid"])
except: except Exception:
pass pass
return None return None

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.model import BaseModel, ModelField from snek.system.model import BaseModel, ModelField

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.model import BaseModel, ModelField from snek.system.model import BaseModel, ModelField

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from datetime import datetime, timezone from datetime import datetime, timezone
from snek.model.user import UserModel from snek.model.user import UserModel

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.model import BaseModel, ModelField from snek.system.model import BaseModel, ModelField

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.model import BaseModel, ModelField from snek.system.model import BaseModel, ModelField

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import mimetypes import mimetypes
from snek.system.model import BaseModel, ModelField from snek.system.model import BaseModel, ModelField

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
# models/forum.py # models/forum.py
from snek.system.model import BaseModel, ModelField from snek.system.model import BaseModel, ModelField

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.model import BaseModel, ModelField from snek.system.model import BaseModel, ModelField

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import logging import logging
from snek.system.model import BaseModel, ModelField from snek.system.model import BaseModel, ModelField

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.model import BaseModel, ModelField from snek.system.model import BaseModel, ModelField

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.model import BaseModel, ModelField from snek.system.model import BaseModel, ModelField

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.model import BaseModel, ModelField from snek.system.model import BaseModel, ModelField

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.model import BaseModel, ModelField from snek.system.model import BaseModel, ModelField

View File

@ -1,6 +1,17 @@
# retoor <retoor@molodetz.nl>
import asyncio
import json import json
class DatasetMethod: class DatasetMethod:
pass
class DatasetTable: class DatasetTable:
pass
class WebSocketClient2: class WebSocketClient2:
def __init__(self, uri): def __init__(self, uri):
self.uri = uri self.uri = uri
@ -8,25 +19,40 @@ class WebSocketClient2:
self.websocket = None self.websocket = None
self.receive_queue = asyncio.Queue() 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): def send(self, message: str):
pass
def close(self): def close(self):
class DatasetWrapper(object): pass
class DatasetWrapper:
def __init__(self): def __init__(self):
pass
def commit(self): def commit(self):
pass
def query(self, *args, **kwargs): def query(self, *args, **kwargs):
pass
class DatasetWebSocketView: class DatasetWebSocketView:
def __init__(self): def __init__(self):
self.ws = None 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): def format_result(self, result):
pass
async def send_str(self, msg): async def send_str(self, msg):
pass
def get(self, key): def get(self, key):
pass
def set(self, key, value): def set(self, key, value):
pass
async def run_server(): async def run_server():
pass

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import time import time
from concurrent.futures import ProcessPoolExecutor from concurrent.futures import ProcessPoolExecutor

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import functools import functools
from snek.service.channel import ChannelService from snek.service.channel import ChannelService
@ -41,7 +43,7 @@ def get_services(app):
def get_service(name, app=None): def get_service(name, app=None):
return get_services(app=app)[name] return get_services(app=app)[name]
# Registering all services
register_service("user", UserService) register_service("user", UserService)
register_service("channel_member", ChannelMemberService) register_service("channel_member", ChannelMemberService)
register_service("channel", ChannelService) register_service("channel", ChannelService)

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import pathlib import pathlib
from datetime import datetime from datetime import datetime
@ -13,7 +15,7 @@ class ChannelService(BaseService):
if not folder.exists(): if not folder.exists():
try: try:
folder.mkdir(parents=True, exist_ok=True) folder.mkdir(parents=True, exist_ok=True)
except: except OSError:
pass pass
return folder return folder

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import mimetypes import mimetypes
from snek.system.service import BaseService from snek.system.service import BaseService

View File

@ -1,5 +1,7 @@
from snek.system.service import BaseService # retoor <retoor@molodetz.nl>
from snek.system.model import now from snek.system.model import now
from snek.system.service import BaseService
class ChannelMemberService(BaseService): class ChannelMemberService(BaseService):
@ -14,7 +16,7 @@ class ChannelMemberService(BaseService):
async def get_user_uids(self, channel_uid): async def get_user_uids(self, channel_uid):
async for model in self.mapper.query( 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}, {"channel_uid": channel_uid},
): ):
yield model["user_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 ", "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}, {"from_user": from_user, "to_user": to_user},
): ):
return model return model
async def get_other_dm_user(self, channel_uid, user_uid): async def get_other_dm_user(self, channel_uid, user_uid):

View File

@ -1,18 +1,21 @@
# retoor <retoor@molodetz.nl>
import asyncio
import json
import logging
import pathlib
from concurrent.futures import ProcessPoolExecutor
from snek.system.service import BaseService from snek.system.service import BaseService
from snek.system.template import sanitize_html from snek.system.template import sanitize_html
import time
import asyncio
from concurrent.futures import ProcessPoolExecutor
import json
from jinja2 import Environment, FileSystemLoader logger = logging.getLogger(__name__)
global jinja2_env jinja2_env = None
import pathlib
template_path = pathlib.Path(__file__).parent.parent.joinpath("templates") template_path = pathlib.Path(__file__).parent.parent.joinpath("templates")
def render(context): def render(context):
template =jinja2_env.get_template("message.html") template = jinja2_env.get_template("message.html")
return sanitize_html(template.render(**context)) return sanitize_html(template.render(**context))
@ -27,10 +30,11 @@ class ChannelMessageService(BaseService):
global jinja2_env global jinja2_env
jinja2_env = self.app.jinja2_env jinja2_env = self.app.jinja2_env
self._max_workers = 1 self._max_workers = 1
def get_or_create_executor(self, uid): def get_or_create_executor(self, uid):
if not uid in self._executor_pools: if not uid in self._executor_pools:
self._executor_pools[uid] = ProcessPoolExecutor(max_workers=self._max_workers) 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] return self._executor_pools[uid]
def delete_executor(self, uid): def delete_executor(self, uid):
@ -39,34 +43,6 @@ class ChannelMessageService(BaseService):
del self._executor_pools[uid] del self._executor_pools[uid]
async def maintenance(self): 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: while True:
changed = 0 changed = 0
async for message in self.find(is_final=False): async for message in self.find(is_final=False):
@ -81,6 +57,7 @@ class ChannelMessageService(BaseService):
break break
async def create(self, channel_uid, user_uid, message, is_final=True): 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 = await self.new()
model["channel_uid"] = channel_uid model["channel_uid"] = channel_uid
@ -93,6 +70,9 @@ class ChannelMessageService(BaseService):
record = model.record record = model.record
context.update(record) context.update(record)
user = await self.app.services.user.get(uid=user_uid) 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( context.update(
{ {
"user_uid": user["uid"], "user_uid": user["uid"],
@ -103,15 +83,13 @@ class ChannelMessageService(BaseService):
) )
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
try: 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(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: 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 await super().save(model):
if not self._configured_indexes: if not self._configured_indexes:
if not self.mapper.db["channel_message"].has_index( if not self.mapper.db["channel_message"].has_index(
@ -129,7 +107,9 @@ class ChannelMessageService(BaseService):
self._configured_indexes = True self._configured_indexes = True
if model['is_final']: if model['is_final']:
self.delete_executor(model['uid']) self.delete_executor(model['uid'])
logger.info(f"create: message created successfully uid={model['uid']}, channel={channel_uid}")
return model 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}.") raise Exception(f"Failed to create channel message: {model.errors}.")
async def to_extended_dict(self, message): async def to_extended_dict(self, message):
@ -154,9 +134,13 @@ class ChannelMessageService(BaseService):
} }
async def save(self, model): async def save(self, model):
logger.debug(f"save: starting for uid={model['uid']}, is_final={model['is_final']}")
context = {} context = {}
context.update(model.record) context.update(model.record)
user = await self.app.services.user.get(model["user_uid"]) 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( context.update(
{ {
"user_uid": user["uid"], "user_uid": user["uid"],
@ -167,10 +151,14 @@ class ChannelMessageService(BaseService):
) )
context = json.loads(json.dumps(context, default=str)) context = json.loads(json.dumps(context, default=str))
loop = asyncio.get_event_loop() 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(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) 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']: if model['is_final']:
self.delete_executor(model['uid']) self.delete_executor(model['uid'])
return result return result
@ -219,6 +207,6 @@ class ChannelMessageService(BaseService):
results.append(model) results.append(model)
except Exception as ex: except Exception as ex:
print(ex) logger.error(f"offset query failed: {ex}")
results.sort(key=lambda x: x["created_at"]) results.sort(key=lambda x: x["created_at"])
return results return results

View File

@ -1,17 +1,29 @@
# retoor <retoor@molodetz.nl>
import logging
from snek.system.model import now from snek.system.model import now
from snek.system.service import BaseService from snek.system.service import BaseService
logger = logging.getLogger(__name__)
class ChatService(BaseService): class ChatService(BaseService):
async def finalize(self, message_uid): 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) 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 channel_message["is_final"] = True
await self.services.channel_message.save(channel_message) 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"]) user = await self.services.user.get(uid=channel_message["user_uid"])
channel = await self.services.channel.get(uid=channel_message["channel_uid"]) channel = await self.services.channel.get(uid=channel_message["channel_uid"])
channel["last_message_on"] = now() channel["last_message_on"] = now()
await self.services.channel.save(channel) await self.services.channel.save(channel)
logger.debug(f"finalize: broadcasting message to channel={channel['uid']}")
await self.services.socket.broadcast( await self.services.socket.broadcast(
channel["uid"], channel["uid"],
{ {
@ -28,18 +40,23 @@ class ChatService(BaseService):
"is_final": channel_message["is_final"], "is_final": channel_message["is_final"],
}, },
) )
logger.info(f"finalize: completed for message_uid={message_uid}, channel={channel['uid']}")
await self.app.create_task( await self.app.create_task(
self.services.notification.create_channel_message(message_uid) self.services.notification.create_channel_message(message_uid)
) )
async def send(self, user_uid, channel_uid, message, is_final=True): 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) channel = await self.services.channel.get(uid=channel_uid)
if not channel: if not channel:
logger.error(f"send: channel not found channel_uid={channel_uid}")
raise Exception("Channel not found.") 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_message = await self.services.channel_message.get(
channel_uid=channel_uid,user_uid=user_uid, is_final=False channel_uid=channel_uid,user_uid=user_uid, is_final=False
) )
if channel_message: if channel_message:
logger.debug(f"send: updating existing message uid={channel_message['uid']}")
channel_message["message"] = message channel_message["message"] = message
channel_message["is_final"] = is_final channel_message["is_final"] = is_final
if not channel_message["is_final"]: if not channel_message["is_final"]:
@ -48,15 +65,18 @@ class ChatService(BaseService):
else: else:
await self.services.channel_message.save(channel_message) await self.services.channel_message.save(channel_message)
else: else:
logger.debug(f"send: creating new message in channel={channel_uid}")
channel_message = await self.services.channel_message.create( channel_message = await self.services.channel_message.create(
channel_uid, user_uid, message, is_final channel_uid, user_uid, message, is_final
) )
channel_message_uid = channel_message["uid"] 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) user = await self.services.user.get(uid=user_uid)
channel["last_message_on"] = now() channel["last_message_on"] = now()
await self.services.channel.save(channel) await self.services.channel.save(channel)
logger.debug(f"send: broadcasting message to channel={channel_uid}")
await self.services.socket.broadcast( await self.services.socket.broadcast(
channel_uid, channel_uid,
{ {
@ -73,6 +93,7 @@ class ChatService(BaseService):
"is_final": is_final, "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( await self.app.create_task(
self.services.notification.create_channel_message(channel_message_uid) self.services.notification.create_channel_message(channel_message_uid)
) )

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.docker import ComposeFileManager from snek.system.docker import ComposeFileManager
from snek.system.service import BaseService from snek.system.service import BaseService
@ -116,4 +118,3 @@ class ContainerService(BaseService):
if await super().save(model): if await super().save(model):
return model return model
raise Exception(f"Failed to create container: {model.errors}") raise Exception(f"Failed to create container: {model.errors}")

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import dataset import dataset
from snek.system.service import BaseService from snek.system.service import BaseService

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.service import BaseService from snek.system.service import BaseService

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.system.service import BaseService from snek.system.service import BaseService

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
# services/forum.py # services/forum.py
from snek.system.service import BaseService from snek.system.service import BaseService
import re import re

View File

@ -1,7 +1,13 @@
# retoor <retoor@molodetz.nl>
import logging
from snek.system.markdown import strip_markdown from snek.system.markdown import strip_markdown
from snek.system.model import now from snek.system.model import now
from snek.system.service import BaseService from snek.system.service import BaseService
logger = logging.getLogger(__name__)
class NotificationService(BaseService): class NotificationService(BaseService):
mapper_name = "notification" mapper_name = "notification"
@ -79,6 +85,6 @@ class NotificationService(BaseService):
}, },
) )
except Exception as e: 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() self.app.db.commit()

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import logging import logging
import re import re
from typing import Optional, List from typing import Optional, List

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import base64 import base64
import json import json
import os.path import os.path

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import asyncio import asyncio
import shutil import shutil

View File

@ -16,8 +16,11 @@ def safe_get(obj, key, default=None):
try: try:
if isinstance(obj, dict): if isinstance(obj, dict):
return obj.get(key, default) 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) return getattr(obj, key, default)
except Exception: except (KeyError, TypeError, AttributeError):
return default return default
@ -195,7 +198,9 @@ class SocketService(BaseService):
async def broadcast(self, channel_uid, message): async def broadcast(self, channel_uid, message):
if not channel_uid or message is None: 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 return False
logger.debug(f"broadcast: starting for channel={channel_uid}")
return await self._broadcast(channel_uid, message) return await self._broadcast(channel_uid, message)
async def _broadcast(self, 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): async for user_uid in self.services.channel_member.get_user_uids(channel_uid):
if user_uid: if user_uid:
user_uids_to_send.add(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: except Exception as ex:
logger.warning(f"Broadcast db query failed: {safe_str(ex)}") logger.warning(f"Broadcast db query failed: {safe_str(ex)}")
if not user_uids_to_send: if not user_uids_to_send:
async with self._lock: async with self._lock:
if channel_uid in self.subscriptions: if channel_uid in self.subscriptions:
user_uids_to_send = set(self.subscriptions[channel_uid]) 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: for user_uid in user_uids_to_send:
try: 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: except Exception as ex:
logger.debug(f"Failed to send to user {user_uid}: {safe_str(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 return True
except Exception as ex: except Exception as ex:
logger.warning(f"Broadcast failed: {safe_str(ex)}") logger.warning(f"Broadcast failed: {safe_str(ex)}")

View File

@ -1,6 +1,10 @@
from snek.system.service import BaseService # retoor <retoor@molodetz.nl>
import sqlite3 import sqlite3
from snek.system.service import BaseService
class StatisticsService(BaseService): class StatisticsService(BaseService):
def database(self): def database(self):

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import pathlib import pathlib
from snek.system import security from snek.system import security
@ -78,7 +80,7 @@ class UserService(BaseService):
if not folder.exists(): if not folder.exists():
try: try:
folder.mkdir(parents=True, exist_ok=True) folder.mkdir(parents=True, exist_ok=True)
except: except OSError:
pass pass
return folder return folder
@ -87,7 +89,7 @@ class UserService(BaseService):
if not folder.exists(): if not folder.exists():
try: try:
folder.mkdir(parents=True, exist_ok=True) folder.mkdir(parents=True, exist_ok=True)
except: except OSError:
pass pass
return folder return folder

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import json import json
from snek.system.service import BaseService from snek.system.service import BaseService

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import random import random
from snek.system.service import BaseService from snek.system.service import BaseService

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import asyncio import asyncio
import base64 import base64
import json import json

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.app import Application from snek.app import Application
from IPython import start_ipython from IPython import start_ipython

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import aiohttp import aiohttp
ENABLED = False ENABLED = False

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import logging import logging
from pathlib import Path from pathlib import Path

View File

@ -1,11 +1,4 @@
// Written by retoor@molodetz.nl // retoor <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
import { Schedule } from "./schedule.js"; import { Schedule } from "./schedule.js";
import { EventHandler } from "./event-handler.js"; import { EventHandler } from "./event-handler.js";
@ -164,7 +157,7 @@ export class App extends EventHandler {
_debug = false; _debug = false;
presenceNotification = null; presenceNotification = null;
async set_typing(channel_uid) { async set_typing(channel_uid) {
this.typeEventChannel_uid = channel_uid; this.typeEventChannelUid = channel_uid;
} }
debug() { debug() {
this._debug = !this._debug; this._debug = !this._debug;
@ -176,17 +169,8 @@ export class App extends EventHandler {
await this.rpc.ping(...args); await this.rpc.ping(...args);
this.is_pinging = false; 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) { async forcePing(...arg) {
await this.rpc.ping(...args); await this.rpc.ping(...arg);
} }
starField = null starField = null
constructor() { constructor() {
@ -205,7 +189,7 @@ export class App extends EventHandler {
this.rpc.set_typing(this.typeEventChannelUid); this.rpc.set_typing(this.typeEventChannelUid);
this.typeEventChannelUid = null; this.typeEventChannelUid = null;
} }
}); }, 1000);
const me = this; const me = this;
this.ws.addEventListener("connected", (data) => { this.ws.addEventListener("connected", (data) => {

View File

@ -1,6 +1,11 @@
// retoor <retoor@molodetz.nl>
import { app } from "./app.js"; 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 { FileUploadGrid } from "./file-upload-grid.js";
import { loggerFactory } from "./logger.js";
const log = loggerFactory.getLogger("ChatInput");
class ChatInputComponent extends NjetComponent { class ChatInputComponent extends NjetComponent {
autoCompletions = { autoCompletions = {
@ -504,7 +509,10 @@ textToLeetAdvanced(text) {
flagTyping() { flagTyping() {
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) >= 1) { if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) >= 1) {
this.lastUpdateEvent = new Date(); 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){ }else if(this._leetSpeakAdvanced){
value = this.textToLeetAdvanced(value); 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.value = "";
this.messageUid = null; this.messageUid = null;
this.queuedMessage = null; this.queuedMessage = null;
@ -526,6 +539,7 @@ textToLeetAdvanced(text) {
updateFromInput(value, isFinal = false) { updateFromInput(value, isFinal = false) {
log.debug("updateFromInput called", { valueLength: value?.length, isFinal, liveType: this.liveType, channelUid: this.channelUid });
this.value = value; this.value = value;
@ -533,13 +547,22 @@ textToLeetAdvanced(text) {
if (this.liveType && value[0] !== "/") { if (this.liveType && value[0] !== "/") {
const messageText = this.replaceMentionsWithAuthors(value); 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); this.messageUid = this.sendMessage(this.channelUid, messageText, !this.liveType || isFinal);
return this.messageUid; return this.messageUid;
} }
} }
async sendMessage(channelUid, value, is_final) { 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;
}
} }
} }

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
// Written by retoor@molodetz.nl // 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. // 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.

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
import { app } from "./app.js"; import { app } from "./app.js";
import { EventHandler } from "./event-handler.js"; import { EventHandler } from "./event-handler.js";
@ -30,7 +32,7 @@ export class Container extends EventHandler{
} }
refresh(){ refresh(){
//this._fitAddon.fit(); //this._fitAddon.fit();
this.ws.send("\x0C"); this.ws.send(" ");
} }
toggle(){ toggle(){
this._container.classList.toggle("hidden") this._container.classList.toggle("hidden")
@ -110,4 +112,3 @@ export class Container extends EventHandler{
window.getContainer = function(){ window.getContainer = function(){
return new Container(app.channelUid) return new Container(app.channelUid)
}*/ }*/

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
import { NjetComponent } from "/njet.js"; import { NjetComponent } from "/njet.js";
class WebTerminal extends NjetComponent { class WebTerminal extends NjetComponent {
@ -247,4 +249,3 @@ window.showTerm = function (options) {
customElements.define("web-terminal", WebTerminal); customElements.define("web-terminal", WebTerminal);
export { WebTerminal }; export { WebTerminal };

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
import { NjetComponent } from "/njet.js" import { NjetComponent } from "/njet.js"
class NjetEditor extends NjetComponent { class NjetEditor extends NjetComponent {
@ -266,7 +268,8 @@ Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`;
} }
goToLine(lineNum) { goToLine(lineNum) {
const lines = this.editor.innerText.split('\n'); const lines = this.editor.innerText.split('
');
if (lineNum < 0 || lineNum >= lines.length) return; if (lineNum < 0 || lineNum >= lines.length) return;
let offset = 0; let offset = 0;
@ -279,7 +282,8 @@ Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`;
getCurrentLineInfo() { getCurrentLineInfo() {
const text = this.editor.innerText; const text = this.editor.innerText;
const caretPos = this.getCaretOffset(); const caretPos = this.getCaretOffset();
const lines = text.split('\n'); const lines = text.split('
');
let charCount = 0; let charCount = 0;
for (let i = 0; i < lines.length; i++) { 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]; this.lastDeletedLine = lines[lineIndex];
lines.splice(lineIndex, 1); lines.splice(lineIndex, 1);
if (lines.length === 0) lines.push(''); if (lines.length === 0) lines.push('');
this.editor.innerText = lines.join('\n'); this.editor.innerText = lines.join('
');
this.setCaretOffset(lineStartOffset); this.setCaretOffset(lineStartOffset);
break; break;
@ -414,7 +419,8 @@ Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`;
const lineToPaste = this.yankedLine || this.lastDeletedLine; const lineToPaste = this.yankedLine || this.lastDeletedLine;
if (lineToPaste) { if (lineToPaste) {
lines.splice(lineIndex + 1, 0, 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); this.setCaretOffset(lineStartOffset + lines[lineIndex].length + 1);
} }
break; break;

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
// Written by retoor@molodetz.nl // Written by retoor@molodetz.nl
// This JavaScript class defines a custom HTML element <fancy-button>, which creates a styled, clickable button element with customizable size, text, and URL redirect functionality. // This JavaScript class defines a custom HTML element <fancy-button>, which creates a styled, clickable button element with customizable size, text, and URL redirect functionality.

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
/* A <file-browser> custom element that talks to /api/files */ /* A <file-browser> custom element that talks to /api/files */
import { NjetComponent } from "/njet.js"; import { NjetComponent } from "/njet.js";

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
import { NjetComponent, NjetDialog } from '/njet.js'; import { NjetComponent, NjetDialog } from '/njet.js';
const FUG_ICONS = { const FUG_ICONS = {

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
// Written by retoor@molodetz.nl // 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. // 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.

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
// Written by retoor@molodetz.nl // Written by retoor@molodetz.nl
// The following JavaScript code defines a custom HTML element `<html-frame>` 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. // The following JavaScript code defines a custom HTML element `<html-frame>` 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.

98
src/snek/static/logger.js Normal file
View File

@ -0,0 +1,98 @@
// retoor <retoor@molodetz.nl>
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 };

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
// Written by retoor@molodetz.nl // Written by retoor@molodetz.nl
// This JavaScript class defines a custom HTML element <markdown-frame> that fetches and loads content from a specified URL into a shadow DOM. // This JavaScript class defines a custom HTML element <markdown-frame> that fetches and loads content from a specified URL into a shadow DOM.

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
// Written by retoor@molodetz.nl // 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. // 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.

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
// Written by retoor@molodetz.nl // 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. // 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.

View File

@ -1,10 +1,5 @@
// Written by retoor@molodetz.nl // retoor <retoor@molodetz.nl>
// 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"; import { app } from "./app.js";
const LONG_TIME = 1000 * 60 * 20; const LONG_TIME = 1000 * 60 * 20;
@ -182,6 +177,7 @@ class MessageList extends HTMLElement {
} }
}); });
app.ws.addEventListener("set_typing", (data) => { app.ws.addEventListener("set_typing", (data) => {
if (app._debug) console.debug("set_typing event received:", data);
this.triggerGlow(data.user_uid, data.color); this.triggerGlow(data.user_uid, data.color);
}); });
@ -256,8 +252,8 @@ class MessageList extends HTMLElement {
} }
triggerGlow(uid, color) { triggerGlow(uid, color) {
if (!uid || !color) return; if (color && app.starField) app.starField.glowColor(color);
if (app.starField) app.starField.glowColor(color); if (!uid) return;
let lastElement = null; let lastElement = null;
this.querySelectorAll('.avatar').forEach((el) => { this.querySelectorAll('.avatar').forEach((el) => {
const anchor = el.closest('a'); const anchor = el.closest('a');
@ -285,12 +281,6 @@ class MessageList extends HTMLElement {
upsertMessage(data) { upsertMessage(data) {
let message = this.messageMap.get(data.uid); 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){ if(message && !data.message){
message.parentElement?.removeChild(message); message.parentElement?.removeChild(message);
message = null; message = null;

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
// Written by retoor@molodetz.nl // 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. // 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.

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
class RestClient { class RestClient {
constructor({ baseURL = '', headers = {} } = {}) { constructor({ baseURL = '', headers = {} } = {}) {
this.baseURL = baseURL; this.baseURL = baseURL;

View File

@ -0,0 +1,3 @@
// retoor <retoor@molodetz.nl>

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
Promise.withResolvers = Promise.withResolvers || function() { Promise.withResolvers = Promise.withResolvers || function() {
let resolve, reject; let resolve, reject;
let promise = new Promise((res, rej) => { let promise = new Promise((res, rej) => {

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
export const registerServiceWorker = async (silent = false) => { export const registerServiceWorker = async (silent = false) => {
try { try {
const serviceWorkerRegistration = await navigator.serviceWorker const serviceWorkerRegistration = await navigator.serviceWorker
@ -36,7 +38,9 @@ export const registerServiceWorker = async (silent = false) => {
} catch (error) { } catch (error) {
console.error("Error registering service worker:", error); console.error("Error registering service worker:", error);
if (!silent) { 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);
} }
} }
} }

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
// Written by retoor@molodetz.nl // 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. // This JavaScript class provides functionality to schedule repeated execution of a function or delay its execution using specified intervals and timeouts.

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
function isClientOpen(url) { function isClientOpen(url) {
return clients.matchAll().then((matchedClients) => { return clients.matchAll().then((matchedClients) => {
return matchedClients.some((matchedClient) => { return matchedClients.some((matchedClient) => {

View File

@ -1,6 +1,9 @@
// retoor <retoor@molodetz.nl> // retoor <retoor@molodetz.nl>
import { EventHandler } from "./event-handler.js"; import { EventHandler } from "./event-handler.js";
import { loggerFactory } from "./logger.js";
const log = loggerFactory.getLogger("Socket");
function createPromiseWithResolvers() { function createPromiseWithResolvers() {
let resolve, reject; let resolve, reject;
@ -45,25 +48,30 @@ export class Socket extends EventHandler {
try { try {
this.url = new URL("/rpc.ws", window.location.origin); this.url = new URL("/rpc.ws", window.location.origin);
this.url.protocol = this.url.protocol.replace("http", "ws"); this.url.protocol = this.url.protocol.replace("http", "ws");
log.info("Socket initializing", { url: this.url.toString() });
this.connect(); this.connect();
} catch (e) { } catch (e) {
console.error("Socket initialization failed:", e); log.error("Socket initialization failed", e);
} }
} }
connect() { connect() {
if (this._isDestroyed) { if (this._isDestroyed) {
log.warn("Connect called on destroyed socket");
return Promise.reject(new Error("Socket destroyed")); return Promise.reject(new Error("Socket destroyed"));
} }
if (this.ws && (this.isConnected || this.isConnecting)) { if (this.ws && (this.isConnected || this.isConnecting)) {
log.debug("Already connected or connecting");
return this.connection ? this.connection.promise : Promise.resolve(this); return this.connection ? this.connection.promise : Promise.resolve(this);
} }
try { try {
log.info("Connecting to WebSocket server");
this._cleanup(); this._cleanup();
if (!this.connection || this.connection.resolved) { if (!this.connection || this.connection.resolved) {
this.connection = createPromiseWithResolvers(); this.connection = createPromiseWithResolvers();
} }
if (!this.url) { if (!this.url) {
log.error("URL not initialized");
this.connection.reject(new Error("URL not initialized")); this.connection.reject(new Error("URL not initialized"));
return this.connection.promise; return this.connection.promise;
} }
@ -71,30 +79,31 @@ export class Socket extends EventHandler {
this.ws.addEventListener("open", () => { this.ws.addEventListener("open", () => {
try { try {
this._reconnectAttempts = 0; this._reconnectAttempts = 0;
log.info("WebSocket connection established");
if (this.connection && !this.connection.resolved) { if (this.connection && !this.connection.resolved) {
this.connection.resolved = true; this.connection.resolved = true;
this.connection.resolve(this); this.connection.resolve(this);
} }
this.emit("connected"); this.emit("connected");
} catch (e) { } catch (e) {
console.error("Open handler error:", e); log.error("Open handler error", e);
} }
}); });
this.ws.addEventListener("close", (event) => { this.ws.addEventListener("close", (event) => {
try { try {
const reason = event.reason || "Connection closed"; const reason = event.reason || "Connection closed";
console.log("Connection closed:", reason); log.info("Connection closed", { reason, code: event.code });
this._handleDisconnect(); this._handleDisconnect();
} catch (e) { } catch (e) {
console.error("Close handler error:", e); log.error("Close handler error", e);
} }
}); });
this.ws.addEventListener("error", (e) => { this.ws.addEventListener("error", (e) => {
try { try {
console.error("Connection error:", e); log.error("Connection error", e);
this._handleDisconnect(); this._handleDisconnect();
} catch (ex) { } catch (ex) {
console.error("Error handler error:", ex); log.error("Error handler error", ex);
} }
}); });
this.ws.addEventListener("message", (e) => { this.ws.addEventListener("message", (e) => {
@ -102,32 +111,34 @@ export class Socket extends EventHandler {
}); });
return this.connection.promise; return this.connection.promise;
} catch (e) { } catch (e) {
console.error("Connect failed:", e); log.error("Connect failed", e);
return Promise.reject(e); return Promise.reject(e);
} }
} }
_handleMessage(e) { _handleMessage(e) {
if (!e || !e.data) { if (!e || !e.data) {
log.debug("Empty message received");
return; return;
} }
try { try {
if (e.data instanceof Blob || e.data instanceof ArrayBuffer) { if (e.data instanceof Blob || e.data instanceof ArrayBuffer) {
console.warn("Binary data not supported"); log.warn("Binary data not supported");
return; return;
} }
let data; let data;
try { try {
data = JSON.parse(e.data); data = JSON.parse(e.data);
} catch (parseError) { } catch (parseError) {
console.error("Failed to parse message:", parseError); log.error("Failed to parse message", { error: parseError, raw: e.data.substring(0, 200) });
return; return;
} }
log.debug("Message received", { callId: data?.callId, event: data?.event, channel_uid: data?.channel_uid });
if (data) { if (data) {
this.onData(data); this.onData(data);
} }
} catch (error) { } 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) { async sendJson(data) {
if (this._isDestroyed) { if (this._isDestroyed) {
log.error("sendJson called on destroyed socket");
throw new Error("Socket destroyed"); throw new Error("Socket destroyed");
} }
if (!data) { if (!data) {
log.error("sendJson called with no data");
throw new Error("No data to send"); throw new Error("No data to send");
} }
try { try {
await this.connect(); await this.connect();
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
log.error("sendJson: WebSocket not open", { readyState: this.ws?.readyState });
throw new Error("WebSocket not open"); throw new Error("WebSocket not open");
} }
const jsonStr = JSON.stringify(data); const jsonStr = JSON.stringify(data);
log.debug("Sending JSON", { method: data.method, callId: data.callId, argsLength: data.args?.length });
this.ws.send(jsonStr); this.ws.send(jsonStr);
} catch (e) { } catch (e) {
console.error("sendJson error:", e); log.error("sendJson error", e);
throw e; throw e;
} }
} }
async call(method, ...args) { async call(method, ...args) {
if (this._isDestroyed) { if (this._isDestroyed) {
log.error("RPC call on destroyed socket", { method });
return Promise.reject(new Error("Socket destroyed")); return Promise.reject(new Error("Socket destroyed"));
} }
if (!method || typeof method !== "string") { if (!method || typeof method !== "string") {
log.error("Invalid RPC method name", { method });
return Promise.reject(new Error("Invalid method name")); return Promise.reject(new Error("Invalid method name"));
} }
const callId = this.generateCallId(); const callId = this.generateCallId();
@ -343,9 +360,12 @@ export class Socket extends EventHandler {
method, method,
args: args || [], args: args || [],
}; };
log.info("RPC call initiated", { method, callId, argsCount: args.length });
const startTime = Date.now();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
try { try {
log.error("RPC call timeout", { method, callId, elapsed: Date.now() - startTime });
this._pendingCalls.delete(callId); this._pendingCalls.delete(callId);
this.removeEventListener(callId, handler); this.removeEventListener(callId, handler);
reject(new Error(`RPC call timeout: ${method}`)); reject(new Error(`RPC call timeout: ${method}`));
@ -356,26 +376,32 @@ export class Socket extends EventHandler {
this._pendingCalls.set(callId, timeoutId); this._pendingCalls.set(callId, timeoutId);
const handler = (response) => { const handler = (response) => {
try { try {
const elapsed = Date.now() - startTime;
clearTimeout(timeoutId); clearTimeout(timeoutId);
this._pendingCalls.delete(callId); this._pendingCalls.delete(callId);
if (response && !response.success && response.error) { if (response && !response.success && response.error) {
log.error("RPC call failed", { method, callId, elapsed, error: response.error });
reject(new Error(response.error)); reject(new Error(response.error));
} else { } else {
log.debug("RPC call completed", { method, callId, elapsed, success: true });
resolve(response ? response.data : null); resolve(response ? response.data : null);
} }
} catch (e) { } catch (e) {
log.error("RPC handler error", { method, callId, error: e });
reject(e); reject(e);
} }
}; };
try { try {
this.addEventListener(callId, handler, { once: true }); this.addEventListener(callId, handler, { once: true });
this.sendJson(callData).catch((e) => { this.sendJson(callData).catch((e) => {
log.error("RPC sendJson failed", { method, callId, error: e });
clearTimeout(timeoutId); clearTimeout(timeoutId);
this._pendingCalls.delete(callId); this._pendingCalls.delete(callId);
this.removeEventListener(callId, handler); this.removeEventListener(callId, handler);
reject(e); reject(e);
}); });
} catch (e) { } catch (e) {
log.error("RPC call setup error", { method, callId, error: e });
clearTimeout(timeoutId); clearTimeout(timeoutId);
this._pendingCalls.delete(callId); this._pendingCalls.delete(callId);
reject(e); reject(e);

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
class STTButton extends HTMLElement { class STTButton extends HTMLElement {
static get observedAttributes() { return ['target']; } static get observedAttributes() { return ['target']; }
@ -92,9 +94,12 @@ class STTButton extends HTMLElement {
committed += punctuated + ' '; committed += punctuated + ' ';
if (this.targetEl) { if (this.targetEl) {
this.targetEl.focus(); this.targetEl.focus();
punctuated = punctuated.replace(/\./g, ".\n") punctuated = punctuated.replace(/\./g, ".
.replace(/\?/g, "?\n") ")
.replace(/\!/g, "!\n"); .replace(/\?/g, "?
")
.replace(/\!/g, "!
");
this.targetEl.value = punctuated; // punctuated; this.targetEl.value = punctuated; // punctuated;
this.simulateTypingWithEvents(this.targetEl, ' ', 0).then(() => { this.simulateTypingWithEvents(this.targetEl, ' ', 0).then(() => {
@ -178,4 +183,3 @@ class STTButton extends HTMLElement {
} }
customElements.define('stt-button', STTButton); customElements.define('stt-button', STTButton);

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
class SnekSpeaker extends HTMLElement { class SnekSpeaker extends HTMLElement {
_enabled = false _enabled = false
@ -66,4 +68,3 @@ class SnekSpeaker extends HTMLElement {
// Define the element // Define the element
customElements.define('snek-speaker', SnekSpeaker); customElements.define('snek-speaker', SnekSpeaker);

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
// Written by retoor@molodetz.nl // Written by retoor@molodetz.nl
// This class defines a custom HTML element for an upload button with integrated file upload functionality using XMLHttpRequest. // This class defines a custom HTML element for an upload button with integrated file upload functionality using XMLHttpRequest.

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
class UserList extends HTMLElement { class UserList extends HTMLElement {
constructor() { constructor() {
super(); super();

View File

@ -1,22 +1,35 @@
# retoor <retoor@molodetz.nl>
class DatasetWebSocketView: class DatasetWebSocketView:
def __init__(self): def __init__(self):
self.ws = None 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): def format_result(self, result):
pass
async def send_str(self, msg): async def send_str(self, msg):
pass
def get(self, key): def get(self, key):
pass
def set(self, key, value): def set(self, key, value):
class BroadCastSocketView: pass
class BroadCastSocketView:
def __init__(self): def __init__(self):
self.ws = None self.ws = None
def format_result(self, result): def format_result(self, result):
pass
async def send_str(self, msg): async def send_str(self, msg):
pass
def get(self, key): def get(self, key):
pass
def set(self, key, value): def set(self, key, value):
pass

Some files were not shown because too many files have changed in this diff Show More