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
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]
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"

View File

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

View File

@ -1,18 +1,18 @@
import logging
logging.basicConfig(level=logging.INFO)
# retoor <retoor@molodetz.nl>
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)

View File

@ -1,21 +1,15 @@
# retoor <retoor@molodetz.nl>
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:

View File

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

View File

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

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import asyncio
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
from snek.model.repository import RepositoryModel
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.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 []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,17 @@
# retoor <retoor@molodetz.nl>
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

View File

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

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
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)

View File

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

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
import mimetypes
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.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):

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

View File

@ -1,17 +1,29 @@
# retoor <retoor@molodetz.nl>
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)
)

View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
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}")

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,13 @@
# retoor <retoor@molodetz.nl>
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()

View File

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

View File

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

View File

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

View File

@ -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)}")

View File

@ -1,5 +1,9 @@
from snek.system.service import BaseService
import sqlite3
# retoor <retoor@molodetz.nl>
import sqlite3
from snek.system.service import BaseService
class StatisticsService(BaseService):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <retoor@molodetz.nl>
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) => {

View File

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

View File

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

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
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)
}*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
// 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
// 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
// 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";
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;

View File

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

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
class RestClient {
constructor({ baseURL = '', headers = {} } = {}) {
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() {
let resolve, reject;
let promise = new Promise((res, rej) => {
@ -5,4 +7,4 @@ Promise.withResolvers = Promise.withResolvers || function() {
reject = rej;
});
return { promise, resolve, reject };
}
}

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
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);
}
}
}

View File

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

View File

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

View File

@ -1,6 +1,9 @@
// retoor <retoor@molodetz.nl>
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);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +1,35 @@
# retoor <retoor@molodetz.nl>
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

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