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:
parent
b710008dbe
commit
8bacd6aa3f
@ -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.
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
"""
|
||||
MIT License
|
||||
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
104
src/snek/app.py
104
src/snek/app.py
@ -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
|
||||
if not request.app.session.get("uid"):
|
||||
except ValueError:
|
||||
return response
|
||||
user = await request.app.services.user.get(uid=request.app.session.get("uid"))
|
||||
if not request.session.get("uid"):
|
||||
return response
|
||||
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))
|
||||
@ -197,7 +194,6 @@ class Application(BaseApplication):
|
||||
self.on_startup.append(self.prepare_database)
|
||||
self.on_startup.append(self.create_default_forum)
|
||||
|
||||
|
||||
@property
|
||||
def uptime_seconds(self):
|
||||
return (datetime.now() - self.time_start).total_seconds()
|
||||
@ -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,10 +248,9 @@ 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")
|
||||
self.db.query("PRAGMA syncnorm=off")
|
||||
@ -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,10 +418,6 @@ 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(
|
||||
@ -452,20 +425,18 @@ class Application(BaseApplication):
|
||||
)
|
||||
|
||||
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", "")
|
||||
|
||||
@ -506,13 +477,12 @@ class Application(BaseApplication):
|
||||
|
||||
@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
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import pathlib
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import asyncio
|
||||
|
||||
from snek.app import app
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
# forum_app.py
|
||||
import aiohttp.web
|
||||
from snek.view.forum import ForumIndexView, ForumView, ForumWebSocketView
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.app import app
|
||||
|
||||
application = app
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import functools
|
||||
|
||||
from snek.mapper.channel import ChannelMapper
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.model.channel import ChannelModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.model.channel_attachment import ChannelAttachmentModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.model.channel_member import ChannelMemberModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.model.channel_message import ChannelMessageModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.model.container import Container
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.model.drive import DriveModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.model.drive_item import DriveItemModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.model.notification import NotificationModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import logging
|
||||
from snek.model.profile_page import ProfilePageModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.model.push_registration import PushRegistrationModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.model.repository import RepositoryModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
|
||||
@ -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 []
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.model.user_property import UserPropertyModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from snek.model.user import UserModel
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import mimetypes
|
||||
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
# models/forum.py
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import logging
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import time
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import mimetypes
|
||||
|
||||
from snek.system.service import BaseService
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
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"],
|
||||
@ -167,10 +151,14 @@ class ChannelMessageService(BaseService):
|
||||
)
|
||||
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
|
||||
|
||||
@ -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)
|
||||
)
|
||||
|
||||
@ -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}")
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import dataset
|
||||
|
||||
from snek.system.service import BaseService
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.service import BaseService
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.system.service import BaseService
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
# services/forum.py
|
||||
from snek.system.service import BaseService
|
||||
import re
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Optional, List
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os.path
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import asyncio
|
||||
import shutil
|
||||
|
||||
|
||||
@ -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)}")
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
from snek.system.service import BaseService
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import sqlite3
|
||||
|
||||
from snek.system.service import BaseService
|
||||
|
||||
|
||||
class StatisticsService(BaseService):
|
||||
|
||||
def database(self):
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import json
|
||||
|
||||
from snek.system.service import BaseService
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import random
|
||||
|
||||
from snek.system.service import BaseService
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from snek.app import Application
|
||||
from IPython import start_ipython
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import aiohttp
|
||||
|
||||
ENABLED = False
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -175,18 +168,9 @@ export class App extends EventHandler {
|
||||
this.is_pinging = true;
|
||||
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) => {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
}*/
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
/* A <file-browser> custom element that talks to /api/files */
|
||||
import { NjetComponent } from "/njet.js";
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import { NjetComponent, NjetDialog } from '/njet.js';
|
||||
|
||||
const FUG_ICONS = {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
98
src/snek/static/logger.js
Normal 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 };
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
class RestClient {
|
||||
constructor({ baseURL = '', headers = {} } = {}) {
|
||||
this.baseURL = baseURL;
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
Promise.withResolvers = Promise.withResolvers || function() {
|
||||
let resolve, reject;
|
||||
let promise = new Promise((res, rej) => {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
function isClientOpen(url) {
|
||||
return clients.matchAll().then((matchedClients) => {
|
||||
return matchedClients.some((matchedClient) => {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
class UserList extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user