Compare commits

..
Author SHA1 Message Date
retoor bde3819510 Update. 2025-06-01 00:24:45 +02:00
retoor 097889ba3f Make database asnyc. 2025-05-31 23:30:41 +02:00
retoor 4854d40508 Update. 2025-05-31 13:52:04 +02:00
retoor 7dd3133475 Merge pull request 'Add URL embedding functionality with metadata extraction and responsive design' (#48) from BordedDev/snek:feat/url-embedding into main
Reviewed-on: #48
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-05-30 19:53:22 +02:00
retoor 24dfa39f91 Update chat input. 2025-05-30 19:32:18 +02:00
BordedDev 7ec65f7c12 Cleaned up imports 2025-05-30 02:14:45 +02:00
BordedDev 4f8edef42b Add URL embedding functionality with metadata extraction and responsive design 2025-05-30 02:13:36 +02:00
retoor 7818410d55 Upddated interval. 2025-05-29 02:42:31 +02:00
retoor 1762191b03 Update. 2025-05-28 21:49:30 +02:00
retoor 2df92e809e Added iinput mode. 2025-05-28 13:40:12 +02:00
retoor 59a8d32e40 Added iinput mode. 2025-05-28 13:36:43 +02:00
retoor c3b3963760 Fix cross typing. 2025-05-28 12:31:17 +02:00
retoor a0cd39e3bc Fix cross typing. 2025-05-28 12:29:01 +02:00
retoor e48b2258e0 Made live typing default. 2025-05-28 11:38:16 +02:00
retoor 35aaf8824f Fixed directory does not exist bug. 2025-05-28 10:40:05 +02:00
retoor 76c69ca3ec Is finalized 2025-05-27 23:14:06 +02:00
retoor 9994225911 Update sort. 2025-05-27 22:22:32 +02:00
retoor 03f699e448 Updated online users. 2025-05-27 22:16:41 +02:00
retoor 2fd01a5ab7 Stars update. 2025-05-27 14:28:07 +02:00
retoor 96629113f1 Most recent users. 2025-05-27 14:03:01 +02:00
retoor 973afa0cc2 Scrolled to bottom fix. 2025-05-27 12:33:04 +02:00
retoor 9bc55e771a Scrolled to bottom fix. 2025-05-27 12:30:42 +02:00
retoor 40a292d05e Scrolled to bottom fix. 2025-05-27 12:21:40 +02:00
retoor bf723db2cc Scrolled to bottom fix. 2025-05-27 12:16:47 +02:00
retoor 46052172b2 Scrolled to bottom fix. 2025-05-27 12:14:02 +02:00
retoor 4f777f0003 Scrolled to bottom fix. 2025-05-27 12:12:03 +02:00
retoor b5e1ba72d0 Scrolled to bottom fix. 2025-05-27 12:10:26 +02:00
retoor df120098f9 Scrolled to bottom fix. 2025-05-27 12:08:53 +02:00
retoor 1c1d578db7 Fixed mentions. 2025-05-27 12:06:35 +02:00
retoor 69352fe0b5 Fixed mentions. 2025-05-27 11:45:23 +02:00
retoor fb3980dad0 Removed last-message. 2025-05-27 11:03:25 +02:00
retoor 6c21a1e619 Async load users. 2025-05-27 10:57:18 +02:00
retoor 112c0dc70a Update channel message. 2025-05-27 10:50:09 +02:00
retoor 538a9ce25d Scrolled to bottom fix. 2025-05-27 10:43:44 +02:00
retoor cdc3d10df5 Moved method. 2025-05-27 10:33:00 +02:00
retoor 1b150e3e64 Moved method. 2025-05-27 10:33:00 +02:00
retoor 27dccc324a Moved method. 2025-05-27 10:33:00 +02:00
retoor 1bb68ab33b Moved method. 2025-05-27 10:33:00 +02:00
retoor 3c6ea15d47 Update render to include user info. 2025-05-27 10:33:00 +02:00
retoor 36e663e1ed added animal view. 2025-05-27 10:33:00 +02:00
retoor 8d2e0381a7 Merge pull request 'Enhance mobile responsiveness by updating viewport settings and restructuring layout' (#47) from BordedDev/snek:bugfix/page-resize-on-mobile into main
Reviewed-on: #47
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-05-27 00:27:33 +02:00
retoor f67d7b35f1 Merge pull request 'Potential missing letter fix' (#46) from BordedDev/snek:bugfix/potential-lag-fix into main
Reviewed-on: #46
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-05-27 00:25:56 +02:00
BordedDev 9ec62f7471 Enhance mobile responsiveness by updating viewport settings and restructuring layout 2025-05-27 00:23:41 +02:00
retoor 6bbbc41360 Update. 2025-05-25 20:50:32 +02:00
retoor d3844ac7a7 Resoted avatars. 2025-05-25 19:01:28 +02:00
retoor 9378e95a5b Resoted avatars. 2025-05-25 18:55:39 +02:00
retoor 5e4c4ce228 Resoted avatars. 2025-05-25 18:51:55 +02:00
retoor ffc373db62 Channel support. 2025-05-25 12:30:43 +02:00
retoor 60266bf0dc Channel support. 2025-05-25 12:30:26 +02:00
BordedDev 8393a80022 Potential missing letter fix 2025-05-25 02:46:49 +02:00
retoor 234edf4756 Merge pull request 'Fixed double messaging when live typing' (#45) from BordedDev/snek:bugfix/live-typing into main
Reviewed-on: #45
2025-05-25 02:29:04 +02:00
BordedDev 5fd401bfb6 Fixed double messaging when live typing 2025-05-25 02:23:52 +02:00
retoor 5663a5f376 Update. 2025-05-25 01:28:42 +02:00
retoor 81327a9e20 Update. 2025-05-25 01:25:48 +02:00
retoor 2a5b9ad276 Merge pull request 'Fix CSS selector for the last message display' (#44) from BordedDev/snek:bugfix/fix-last-message-time-reply into main
Reviewed-on: #44
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-05-24 18:46:43 +02:00
BordedDev 662e71c621 Fix CSS selector for the last message display 2025-05-24 18:41:12 +02:00
retoor 636adfd997 Update performance. 2025-05-24 15:00:26 +02:00
retoor b94f7a9532 Update. 2025-05-24 14:45:53 +02:00
retoor f954a34384 Update performance. 2025-05-24 14:22:12 +02:00
retoor 6a74263606 Update performance. 2025-05-24 14:18:38 +02:00
retoor 0bf714061c Update. 2025-05-24 12:58:49 +02:00
retoor e4e2e919c2 Update. 2025-05-24 12:58:49 +02:00
retoor 7fe4289f42 Merge pull request 'Made datetime/reply visible when date range is long, also fixes opacity to 1 for the last message's time display' (#39) from BordedDev/snek:bugfix/opacity-for-last-message into main
Reviewed-on: #39
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-05-24 01:48:07 +02:00
BordedDev 3ce866b7da Merge branch 'main' into bugfix/opacity-for-last-message 2025-05-23 18:14:32 +02:00
retoor 43982c16fa Merge pull request 'Fix timestamp parsing by correcting string slicing for start time' (#40) from BordedDev/snek:bugfix/youtube-timestamp into main
Reviewed-on: #40
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-05-23 18:13:50 +02:00
BordedDev e33e4196ab Merge branch 'main' into bugfix/youtube-timestamp 2025-05-23 15:45:56 +02:00
BordedDev 2c506db4e4 Merge branch 'main' into bugfix/opacity-for-last-message 2025-05-23 15:34:30 +02:00
retoor 9b9d356849 update. 2025-05-23 15:26:47 +02:00
retoor 30b7871583 Update. 2025-05-23 07:07:19 +02:00
retoor 1c873b7d02 Update. 2025-05-23 07:02:45 +02:00
retoor 539fb262b2 Update. 2025-05-23 07:00:28 +02:00
retoor 3bf09f9083 Update. 2025-05-23 06:48:18 +02:00
retoor a55d15b635 The force. 2025-05-23 03:01:20 +02:00
retoor 431748c489 Updated dem glow. 2025-05-23 02:39:48 +02:00
retoor a0fb214332 Updated dem glow. 2025-05-23 01:17:41 +02:00
retoor f0545cbf02 Format. 2025-05-23 01:17:41 +02:00
BordedDev a11c336cf5 Fix timestamp parsing by correcting string slicing for start time 2025-05-22 08:10:39 +02:00
BordedDev 6b083f8b1b Added feature to show time and reply when messages are from a long time before the next message 2025-05-22 00:19:39 +02:00
BordedDev 89afbba165 Set opacity to 1 for the last message's time display 2025-05-22 00:00:51 +02:00
retoor b2a4887e23 Merge pull request 'Enhance hover effect for time display and avatar visibility' (#38) from BordedDev/snek:feat/show-time-reply-hover into main
Reviewed-on: #38
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-05-21 23:36:25 +02:00
BordedDev 9f577875f2 Enhance hover effect for time display and avatar visibility 2025-05-21 22:30:15 +02:00
retoor c322d6147a Merge pull request 'bugfix/youtube-embed' (#37) from BordedDev/snek:bugfix/youtube-embed into main
Reviewed-on: #37
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-05-20 22:02:16 +02:00
retoor 87b6b3362d New avatar. 2025-05-20 04:20:10 +02:00
BordedDev d261f54327 Merge branch 'main' into bugfix/youtube-embed 2025-05-20 03:34:05 +02:00
retoor 59a815f85a Update. 2025-05-19 01:35:34 +02:00
retoor 2e837f96c5 Update. 2025-05-19 01:28:22 +02:00
retoor 00fce6bd68 Update. 2025-05-19 01:23:12 +02:00
retoor 8a85cd7990 Update. 2025-05-19 01:13:18 +02:00
retoor db5431d77d Update. 2025-05-19 01:07:17 +02:00
retoor 527b010b24 Added containers. 2025-05-19 01:07:17 +02:00
retoor e1727caa5f Update. 2025-05-18 17:48:22 +02:00
retoor c45b61681d Update. 2025-05-18 16:57:14 +02:00
retoor e09652413f Added nice repo system. 2025-05-18 16:55:02 +02:00
BordedDev 0f337e569f Fixed gif resizing 2025-05-18 14:51:38 +02:00
BordedDev 59a2668c8c Merge branch 'main' into bugfix/youtube-embed 2025-05-18 03:26:53 +02:00
retoor e79abf4a26 Update stars. 2025-05-17 17:46:59 +02:00
BordedDev 53811ca9b2 Re-added webp fallback 2025-05-17 13:55:46 +02:00
BordedDev 1bed47fbf5 Re-added webp fallback 2025-05-17 13:29:25 +02:00
BordedDev ffb22165da Fix YouTube embed parsing and add support for start time; handle missing channel attachments 2025-05-17 13:23:32 +02:00
retoor 48c3daf398 Update. 2025-05-17 00:54:15 +02:00
retoor c0b4ba715c t: 2025-05-17 00:53:27 +02:00
retoor 00557ec9ea Update. 2025-05-16 01:38:42 +02:00
retoor c387225a6e Update. 2025-05-16 00:41:40 +02:00
retoor 93462d4c4b Update. 2025-05-16 00:32:54 +02:00
retoor c5b55399a1 UPdate. 2025-05-16 00:04:19 +02:00
retoor 79c39828f0 update. 2025-05-15 23:30:23 +02:00
retoor dd80f3732b Update. 2025-05-15 23:16:28 +02:00
retoor 25d109beed Update. 2025-05-15 19:32:40 +02:00
retoor db6d6c0106 Update live type. 2025-05-15 13:18:53 +02:00
retoor af1cf4f5ae Push 2025-05-13 23:33:24 +02:00
retoor 0ea0cd96db Update. 2025-05-13 22:54:21 +02:00
retoor 3858dcbd62 Update. 2025-05-13 21:25:49 +02:00
retoor b55d74fb12 Update. 2025-05-13 20:35:42 +02:00
retoor a21e3590ef UPdate. 2025-05-13 20:30:48 +02:00
retoor 319c1b1b52 UPdate. 2025-05-13 20:28:31 +02:00
retoor 964a747f42 UPdate. 2025-05-13 20:27:26 +02:00
retoor 12d2870424 UPdate. 2025-05-13 20:25:00 +02:00
retoor 015b188d5e UPdate. 2025-05-13 20:24:29 +02:00
retoor 8cd2f16c5c UPdate. 2025-05-13 20:20:43 +02:00
retoor d09055986e UPdate. 2025-05-13 20:18:47 +02:00
retoor 2e324ff118 Update. 2025-05-13 19:13:50 +02:00
retoor adad5ed4fe Update. 2025-05-13 19:08:18 +02:00
retoor ba3152f553 Update. 2025-05-13 18:33:05 +02:00
retoor a4bea94495 Windows friendly solution. 2025-05-13 18:32:59 +02:00
retoor ac2f68f93f Merge pull request 'Add image conversion and resizing support in channel attachments' (#36) from BordedDev/snek:feat/image-conversion-resizing into main
Reviewed-on: #36
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-05-13 18:19:57 +02:00
BordedDev f156a153de Add image conversion and resizing support in channel attachments 2025-05-12 01:47:54 +02:00
retoor c48b84bf3a Update. 2025-05-11 07:52:58 +02:00
retoor 01846bf23f Update. 2025-05-11 07:52:22 +02:00
retoor 2c90044185 xxx 2025-05-10 21:44:58 +02:00
retoor 4d7566de9b Update. 2025-05-10 20:40:56 +02:00
retoor 9133b7c3ce Update. 2025-05-10 20:38:32 +02:00
retoor 3412aa0bf0 Update. 2025-05-10 15:08:28 +02:00
retoor f0591d4939 UPdate. 2025-05-10 15:03:50 +02:00
retoor dd108c2004 Update. 2025-05-09 17:37:53 +02:00
retoor 44ac1d2bfa Update. 2025-05-09 15:55:51 +02:00
retoor 1616e4edb9 revert 17c6124a57
revert Minify.
2025-05-09 14:57:22 +02:00
retoor 4c34d7eda5 New stuff. 2025-05-09 14:30:53 +02:00
152 changed files with 12519 additions and 3259 deletions
-1
View File
@@ -20,7 +20,6 @@ serve: run
run: run:
.venv/bin/snek serve .venv/bin/snek serve
#$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload
install: ubuntu install: ubuntu
python3.12 -m venv .venv python3.12 -m venv .venv
+6 -2
View File
@@ -18,7 +18,8 @@ dependencies = [
"lxml", "lxml",
"IPython", "IPython",
"shed", "shed",
"app @ git+https://retoor.molodetz.nl/retoor/app", "app @ git+https://retoor.molodetz.nl/retoor/app.git",
"zhurnal @git+https://retoor.molodetz.nl/retoor/zhurnal.git",
"beautifulsoup4", "beautifulsoup4",
"gunicorn", "gunicorn",
"imgkit", "imgkit",
@@ -33,7 +34,10 @@ dependencies = [
"PyJWT", "PyJWT",
"multiavatar", "multiavatar",
"gitpython", "gitpython",
"uvloop" 'uvloop; platform_system != "Windows"',
"humanize",
"Pillow",
"pillow-heif",
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
+1
View File
@@ -0,0 +1 @@
+50 -14
View File
@@ -1,21 +1,57 @@
_D='Database path for the application' import click
_C='snek.db' import uvloop
_B='--db_path'
_A=True
import click,uvloop
from aiohttp import web from aiohttp import web
import asyncio import asyncio
from snek.app import Application from snek.app import Application
from IPython import start_ipython from IPython import start_ipython
import sqlite3
import pathlib
import shutil
@click.group() @click.group()
def cli():0 def cli():
pass
@cli.command() @cli.command()
@click.option('--port',default=8081,show_default=_A,help='Port to run the application on') @click.option('--db_path',default="snek.db", help='Database to initialize if not exists.')
@click.option('--host',default='0.0.0.0',show_default=_A,help='Host to run the application on') @click.option('--source',default=None, help='Database to initialize if not exists.')
@click.option(_B,default=_C,show_default=_A,help=_D) def init(db_path,source):
def serve(port,host,db_path):asyncio.set_event_loop_policy(uvloop.EventLoopPolicy());web.run_app(Application(db_path=f"sqlite:///{db_path}"),port=port,host=host) if source and pathlib.Path(source).exists():
print(f"Copying {source} to {db_path}")
shutil.copy2(source,db_path)
print("Database initialized.")
return
if pathlib.Path(db_path).exists():
return
print(f"Initializing database at {db_path}")
db = sqlite3.connect(db_path)
db.cursor().executescript(
pathlib.Path(__file__).parent.joinpath("schema.sql").read_text()
)
db.commit()
db.close()
print("Database initialized.")
@cli.command() @cli.command()
@click.option(_B,default=_C,show_default=_A,help=_D) @click.option('--port', default=8081, show_default=True, help='Port to run the application on')
def shell(db_path):A=Application(db_path=f"sqlite:///{db_path}");start_ipython(argv=[],user_ns={'app':A}) @click.option('--host', default='0.0.0.0', show_default=True, help='Host to run the application on')
def main():cli() @click.option('--db_path', default='snek.db', show_default=True, help='Database path for the application')
if __name__=='__main__':main() def serve(port, host, db_path):
#init(db_path)
#asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
web.run_app(
Application(db_path=f"sqlite:///{db_path}"), port=port, host=host
)
@cli.command()
@click.option('--db_path', default='snek.db', show_default=True, help='Database path for the application')
def shell(db_path):
app = Application(db_path=f"sqlite:///{db_path}")
start_ipython(argv=[], user_ns={'app': app})
def main():
cli()
if __name__ == "__main__":
main()
+342 -81
View File
@@ -1,32 +1,41 @@
_G='name' import asyncio
_F='static' import logging
_E='user' import pathlib
_D=None import time
_C=True import uuid
_B='channel_uid' from datetime import datetime
_A='uid' from snek import snode
import asyncio,logging,pathlib,time,uuid
from snek.view.threads import ThreadsView from snek.view.threads import ThreadsView
import json
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from aiohttp import web from aiohttp import web
from aiohttp_session import get_session as session_get,session_middleware,setup as session_setup from aiohttp_session import (
get_session as session_get,
session_middleware,
setup as session_setup,
)
from aiohttp_session.cookie_storage import EncryptedCookieStorage from aiohttp_session.cookie_storage import EncryptedCookieStorage
from app.app import Application as BaseApplication from app.app import Application as BaseApplication
from jinja2 import FileSystemLoader from jinja2 import FileSystemLoader
from snek.sssh import start_ssh_server
from snek.docs.app import Application as DocsApplication from snek.docs.app import Application as DocsApplication
from snek.mapper import get_mappers from snek.mapper import get_mappers
from snek.service import get_services from snek.service import get_services
from snek.system import http from snek.system import http
from snek.system.cache import Cache from snek.system.cache import Cache
from snek.system.markdown import MarkdownExtension from snek.system.markdown import MarkdownExtension
from snek.system.middleware import auth_middleware,cors_middleware from snek.system.middleware import auth_middleware, cors_middleware
from snek.system.profiler import profiler_handler from snek.system.profiler import profiler_handler
from snek.system.template import EmojiExtension,LinkifyExtension,PythonExtension from snek.system.template import EmojiExtension, LinkifyExtension, PythonExtension
from snek.view.about import AboutHTMLView,AboutMDView from snek.view.about import AboutHTMLView, AboutMDView
from snek.view.avatar import AvatarView from snek.view.avatar import AvatarView
from snek.view.docs import DocsHTMLView,DocsMDView from snek.view.docs import DocsHTMLView, DocsMDView
from snek.view.drive import DriveView from snek.view.drive import DriveView
from snek.view.drive import DriveApiView
from snek.view.index import IndexView from snek.view.index import IndexView
from snek.view.login import LoginView from snek.view.login import LoginView
from snek.view.logout import LogoutView from snek.view.logout import LogoutView
@@ -42,79 +51,331 @@ from snek.view.settings.index import SettingsIndexView
from snek.view.settings.profile import SettingsProfileView from snek.view.settings.profile import SettingsProfileView
from snek.view.stats import StatsView from snek.view.stats import StatsView
from snek.view.status import StatusView from snek.view.status import StatusView
from snek.view.terminal import TerminalSocketView,TerminalView from snek.view.terminal import TerminalSocketView, TerminalView
from snek.view.upload import UploadView from snek.view.upload import UploadView
from snek.view.user import UserView from snek.view.user import UserView
from snek.view.web import WebView from snek.view.web import WebView
from snek.view.channel import ChannelAttachmentView
from snek.view.channel import ChannelView
from snek.view.settings.containers import ContainersIndexView, ContainersCreateView, ContainersUpdateView, ContainersDeleteView
from snek.webdav import WebdavApplication from snek.webdav import WebdavApplication
from snek.sgit import GitApplication from snek.sgit import GitApplication
SESSION_KEY=b'c79a0c5fda4b424189c427d28c9f7c34' SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
@web.middleware @web.middleware
async def session_middleware(request,handler):A=request;setattr(A,'session',await session_get(A));B=await handler(A);return B async def session_middleware(request, handler):
setattr(request, "session", await session_get(request))
response = await handler(request)
return response
@web.middleware @web.middleware
async def trailing_slash_middleware(request,handler): async def trailing_slash_middleware(request, handler):
A=request if request.path and not request.path.endswith("/"):
if A.path and not A.path.endswith('/'):raise web.HTTPFound(A.path+'/') # Redirect to the same path with a trailing slash
return await handler(A) raise web.HTTPFound(request.path + "/")
return await handler(request)
class Application(BaseApplication): class Application(BaseApplication):
def __init__(A,*B,**C):D=[cors_middleware,web.normalize_path_middleware(merge_slashes=_C)];A.template_path=pathlib.Path(__file__).parent.joinpath('templates');A.static_path=pathlib.Path(__file__).parent.joinpath(_F);super().__init__(middlewares=D,template_path=A.template_path,client_max_size=5368709120*B,**C);session_setup(A,EncryptedCookieStorage(SESSION_KEY));A.tasks=asyncio.Queue();A._middlewares.append(session_middleware);A._middlewares.append(auth_middleware);A.jinja2_env.add_extension(MarkdownExtension);A.jinja2_env.add_extension(LinkifyExtension);A.jinja2_env.add_extension(PythonExtension);A.jinja2_env.add_extension(EmojiExtension);A.setup_router();A.executor=_D;A.cache=Cache(A);A.services=get_services(app=A);A.mappers=get_mappers(app=A);A.on_startup.append(A.prepare_asyncio);A.on_startup.append(A.prepare_database)
async def prepare_asyncio(A,app):app.executor=ThreadPoolExecutor(max_workers=200);app.loop.set_default_executor(A.executor) def __init__(self, *args, **kwargs):
async def create_task(A,task):await A.tasks.put(task) middlewares = [
async def task_runner(A): cors_middleware,
while _C: web.normalize_path_middleware(merge_slashes=True),
B=await A.tasks.get();A.db.begin() ]
try:C=time.time();await B;D=time.time();print(f"Task {B} took {D-C} seconds");A.tasks.task_done() self.template_path = pathlib.Path(__file__).parent.joinpath("templates")
except Exception as E:print(E) self.static_path = pathlib.Path(__file__).parent.joinpath("static")
A.db.commit() super().__init__(
async def prepare_database(A,app): middlewares=middlewares, template_path=self.template_path, client_max_size=1024*1024*1024*5 *args, **kwargs
C='channel_message';D='channel_member';E='username';B='user_uid';A.db.query('PRAGMA journal_mode=WAL');A.db.query('PRAGMA syncnorm=off') )
try: session_setup(self, EncryptedCookieStorage(SESSION_KEY))
if not A.db[_E].has_index(E):A.db[_E].create_index(E,unique=_C) self.tasks = asyncio.Queue()
if not A.db[D].has_index([_B,B]):A.db[D].create_index([_B,B]) self._middlewares.append(session_middleware)
if not A.db[C].has_index([_B,B]):A.db[C].create_index([_B,B]) self._middlewares.append(auth_middleware)
except:pass self.jinja2_env.add_extension(MarkdownExtension)
await app.services.drive.prepare_all();A.loop.create_task(A.task_runner()) self.jinja2_env.add_extension(LinkifyExtension)
def setup_router(A):A.router.add_get('/',IndexView);A.router.add_static('/',pathlib.Path(__file__).parent.joinpath(_F),name=_F,show_index=_C);A.router.add_view('/profiler.html',profiler_handler);A.router.add_view('/about.html',AboutHTMLView);A.router.add_view('/about.md',AboutMDView);A.router.add_view('/logout.json',LogoutView);A.router.add_view('/logout.html',LogoutView);A.router.add_view('/docs.html',DocsHTMLView);A.router.add_view('/docs.md',DocsMDView);A.router.add_view('/status.json',StatusView);A.router.add_view('/settings/index.html',SettingsIndexView);A.router.add_view('/settings/profile.html',SettingsProfileView);A.router.add_view('/settings/profile.json',SettingsProfileView);A.router.add_view('/web.html',WebView);A.router.add_view('/login.html',LoginView);A.router.add_view('/login.json',LoginView);A.router.add_view('/register.html',RegisterView);A.router.add_view('/register.json',RegisterView);A.router.add_view('/drive/{rel_path:.*}',DriveView);A.router.add_view('/drive.bin',UploadView);A.router.add_view('/drive.bin/{uid}.{ext}',UploadView);A.router.add_view('/search-user.html',SearchUserView);A.router.add_view('/search-user.json',SearchUserView);A.router.add_view('/avatar/{uid}.svg',AvatarView);A.router.add_get('/http-get',A.handle_http_get);A.router.add_get('/http-photo',A.handle_http_photo);A.router.add_get('/rpc.ws',RPCView);A.router.add_view('/channel/{channel}.html',WebView);A.router.add_view('/threads.html',ThreadsView);A.router.add_view('/terminal.ws',TerminalSocketView);A.router.add_view('/terminal.html',TerminalView);A.router.add_view('/drive.json',DriveView);A.router.add_view('/drive/{drive}.json',DriveView);A.router.add_view('/stats.json',StatsView);A.router.add_view('/user/{user}.html',UserView);A.router.add_view('/repository/{username}/{repo_name}',RepositoryView);A.router.add_view('/repository/{username}/{repo_name}/{rel_path:.*}',RepositoryView);A.router.add_view('/settings/repositories/index.html',RepositoriesIndexView);A.router.add_view('/settings/repositories/create.html',RepositoriesCreateView);A.router.add_view('/settings/repositories/repository/{name}/update.html',RepositoriesUpdateView);A.router.add_view('/settings/repositories/repository/{name}/delete.html',RepositoriesDeleteView);A.webdav=WebdavApplication(A);A.git=GitApplication(A);A.add_subapp('/webdav',A.webdav);A.add_subapp('/git',A.git) self.jinja2_env.add_extension(PythonExtension)
async def handle_test(A,request):return await A.render_template('test.html',request,context={_G:'retoor'}) self.jinja2_env.add_extension(EmojiExtension)
async def handle_http_get(C,request):A=request.query.get('url');B=await http.get(A);return web.Response(body=B) self.time_start = datetime.now()
async def handle_http_photo(C,request):A=request.query.get('url');B=await http.create_site_photo(A);return web.Response(body=B.read_bytes(),headers={'Content-Type':'image/png'}) self.ssh_host = "0.0.0.0"
async def render_template(A,template,request,context=_D): self.ssh_port = 2242
I='channels';J='new_count';K='color';L=template;F='last_message_on';D=request;C=context;G=[] self.setup_router()
if not C:C={} self.ssh_server = None
C['rid']=str(uuid.uuid4()) self.sync_service = None
if D.session.get(_A): self.executor = None
async for E in A.services.channel_member.find(user_uid=D.session.get(_A),deleted_at=_D,is_banned=False): self.cache = Cache(self)
B={};M=await A.services.channel_member.get_other_dm_user(E[_B],D.session.get(_A));H=await E.get_channel();N=await H.get_last_message();O=_D self.services = get_services(app=self)
if N:P=await N.get_user();O=P[K] self.mappers = get_mappers(app=self)
B[K]=O;B[F]=H[F];B['is_private']=H['tag']=='dm' self.broadcast_service = None
if M:B[_G]=M['nick'];B[_A]=E[_B] self.user_availability_service_task = None
else:B[_G]=E['label'];B[_A]=E[_B]
B[J]=E[J];G.append(B) self.on_startup.append(self.prepare_asyncio)
G.sort(key=lambda x:x[F]or'',reverse=_C) self.on_startup.append(self.start_user_availability_service)
if I not in C:C[I]=G self.on_startup.append(self.start_ssh_server)
if _E not in C:C[_E]=await A.services.user.get(D.session.get(_A)) self.on_startup.append(self.prepare_database)
A.template_path.joinpath(L);await A.services.user.get_template_path(D.session.get(_A));A.original_loader=A.jinja2_env.loader;A.jinja2_env.loader=await A.get_user_template_loader(D.session.get(_A));Q=await super().render_template(L,D,C);A.jinja2_env.loader=A.original_loader;return Q
async def static_handler(B,request): @property
D=request;E=D.match_info.get('filename','');C=[];F=D.session.get(_A) def uptime_seconds(self):
if F: return (datetime.now() - self.time_start).total_seconds()
A=await B.services.user.get_static_path(F)
if A:C.append(A) @property
for H in B.services.user.get_admin_uids(): def uptime(self):
A=await B.services.user.get_static_path(H) return self._format_uptime(self.uptime_seconds)
if A:C.append(A)
C.append(B.static_path) def _format_uptime(self,seconds):
for G in C: seconds = int(seconds)
if pathlib.Path(G).joinpath(E).exists():return web.FileResponse(pathlib.Path(G).joinpath(E)) days, seconds = divmod(seconds, 86400)
return web.HTTPNotFound() hours, seconds = divmod(seconds, 3600)
async def get_user_template_loader(B,uid=_D): minutes, seconds = divmod(seconds, 60)
C=[]
for D in B.services.user.get_admin_uids(): parts = []
A=await B.services.user.get_template_path(D) if days > 0:
if A:C.append(A) parts.append(f"{days} day{'s' if days != 1 else ''}")
if uid: if hours > 0:
A=await B.services.user.get_template_path(uid) parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
if A:C.append(A) if minutes > 0:
C.append(B.template_path);return FileSystemLoader(C) parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
app=Application(db_path='sqlite:///snek.db') if seconds > 0 or not parts:
async def main():await web._run_app(app,port=8081,host='0.0.0.0') parts.append(f"{seconds} second{'s' if seconds != 1 else ''}")
if __name__=='__main__':asyncio.run(main())
return ", ".join(parts)
async def start_user_availability_service(self, app):
app.user_availability_service_task = asyncio.create_task(app.services.socket.user_availability_service())
async def snode_sync(self, app):
self.sync_service = asyncio.create_task(snode.sync_service(app))
async def start_ssh_server(self, app):
app.ssh_server = await start_ssh_server(app,app.ssh_host,app.ssh_port)
if app.ssh_server:
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)
async def create_task(self, task):
await self.tasks.put(task)
async def task_runner(self):
while True:
task = await self.tasks.get()
self.db.begin()
try:
await task
self.tasks.task_done()
except Exception as ex:
print(ex)
self.db.commit()
async def prepare_database(self, app):
self.db.query("PRAGMA journal_mode=WAL")
self.db.query("PRAGMA syncnorm=off")
try:
if not self.db["user"].has_index("username"):
self.db["user"].create_index("username", unique=True)
if not self.db["channel_member"].has_index(["channel_uid", "user_uid"]):
self.db["channel_member"].create_index(["channel_uid", "user_uid"])
if not self.db["channel_message"].has_index(["channel_uid", "user_uid"]):
self.db["channel_message"].create_index(["channel_uid", "user_uid"])
except:
pass
await app.services.drive.prepare_all()
self.loop.create_task(self.task_runner())
def setup_router(self):
self.router.add_get("/", IndexView)
self.router.add_static(
"/",
pathlib.Path(__file__).parent.joinpath("static"),
name="static",
show_index=True,
)
self.router.add_view("/profiler.html", profiler_handler)
self.router.add_view("/about.html", AboutHTMLView)
self.router.add_view("/about.md", AboutMDView)
self.router.add_view("/logout.json", LogoutView)
self.router.add_view("/logout.html", LogoutView)
self.router.add_view("/docs.html", DocsHTMLView)
self.router.add_view("/docs.md", DocsMDView)
self.router.add_view("/status.json", StatusView)
self.router.add_view("/settings/index.html", SettingsIndexView)
self.router.add_view("/settings/profile.html", SettingsProfileView)
self.router.add_view("/settings/profile.json", SettingsProfileView)
self.router.add_view("/web.html", WebView)
self.router.add_view("/login.html", LoginView)
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)
self.router.add_get("/http-get", self.handle_http_get)
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/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)
self.router.add_view("/terminal.html", TerminalView)
self.router.add_view("/drive.json", DriveApiView)
self.router.add_view("/drive.html", DriveView)
self.router.add_view("/drive/{drive}.json", DriveView)
self.router.add_view("/stats.json", StatsView)
self.router.add_view("/user/{user}.html", UserView)
self.router.add_view("/repository/{username}/{repository}", RepositoryView)
self.router.add_view("/repository/{username}/{repository}/{path:.*}", RepositoryView)
self.router.add_view("/settings/repositories/index.html", RepositoriesIndexView)
self.router.add_view("/settings/repositories/create.html", RepositoriesCreateView)
self.router.add_view("/settings/repositories/repository/{name}/update.html", RepositoriesUpdateView)
self.router.add_view("/settings/repositories/repository/{name}/delete.html", RepositoriesDeleteView)
self.router.add_view("/settings/containers/index.html", ContainersIndexView)
self.router.add_view("/settings/containers/create.html", ContainersCreateView)
self.router.add_view("/settings/containers/container/{uid}/update.html", ContainersUpdateView)
self.router.add_view("/settings/containers/container/{uid}/delete.html", ContainersDeleteView)
self.webdav = WebdavApplication(self)
self.git = GitApplication(self)
self.add_subapp("/webdav", self.webdav)
self.add_subapp("/git",self.git)
#self.router.add_get("/{file_path:.*}", self.static_handler)
async def handle_test(self, request):
return await self.render_template(
"test.html", request, context={"name": "retoor"}
)
async def handle_http_get(self, request: web.Request):
url = request.query.get("url")
content = await http.get(url)
return web.Response(body=content)
async def handle_http_photo(self, request):
url = request.query.get("url")
path = await http.create_site_photo(url)
return web.Response(
body=path.read_bytes(), headers={"Content-Type": "image/png"}
)
# @time_cache_async(60)
async def render_template(self, template, request, context=None):
channels = []
if not context:
context = {}
context["rid"] = str(uuid.uuid4())
if request.session.get("uid"):
async for subscribed_channel in self.services.channel_member.find(
user_uid=request.session.get("uid"), deleted_at=None, is_banned=False
):
parent_object = await subscribed_channel.get_channel()
item = {}
other_user = await self.services.channel_member.get_other_dm_user(
subscribed_channel["channel_uid"], request.session.get("uid")
)
last_message = await parent_object.get_last_message()
color = None
if last_message:
last_message_user = await last_message.get_user()
color = last_message_user["color"]
item["color"] = color
item["last_message_on"] = parent_object["last_message_on"]
item["is_private"] = parent_object["tag"] == "dm"
if other_user:
item["name"] = other_user["nick"]
item["uid"] = subscribed_channel["channel_uid"]
else:
item["name"] = subscribed_channel["label"]
item["uid"] = subscribed_channel["channel_uid"]
item["new_count"] = subscribed_channel["new_count"]
channels.append(item)
channels.sort(key=lambda x: x["last_message_on"] or "", reverse=True)
if "channels" not in context:
context["channels"] = channels
if "user" not in context:
context["user"] = await self.services.user.get(
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")
)
rendered = await super().render_template(template, request, context)
self.jinja2_env.loader = self.original_loader
return rendered
async def static_handler(self, request):
file_name = request.match_info.get('filename', '')
paths = []
uid = request.session.get("uid")
if uid:
user_static_path = await self.services.user.get_static_path(uid)
if user_static_path:
paths.append(user_static_path)
for admin_uid in self.services.user.get_admin_uids():
user_static_path = await self.services.user.get_static_path(admin_uid)
if user_static_path:
paths.append(user_static_path)
paths.append(self.static_path)
for path in paths:
if pathlib.Path(path).joinpath(file_name).exists():
return web.FileResponse(pathlib.Path(path).joinpath(file_name))
return web.HTTPNotFound()
async def get_user_template_loader(self, uid=None):
template_paths = []
for admin_uid in self.services.user.get_admin_uids():
user_template_path = await self.services.user.get_template_path(admin_uid)
if user_template_path:
template_paths.append(user_template_path)
if uid:
user_template_path = await self.services.user.get_template_path(uid)
if user_template_path:
template_paths.append(user_template_path)
template_paths.append(self.template_path)
return FileSystemLoader(template_paths)
app = Application(db_path="sqlite:///snek.db")
async def main():
await web._run_app(app, port=8081, host="0.0.0.0")
if __name__ == "__main__":
asyncio.run(main())
+123
View File
@@ -0,0 +1,123 @@
import asyncio
import sys
class LoadBalancer:
def __init__(self, backend_ports):
self.backend_ports = backend_ports
self.backend_processes = []
self.client_counts = [0] * len(backend_ports)
self.lock = asyncio.Lock()
async def start_backend_servers(self,port,workers):
for x in range(workers):
port += 1
process = await asyncio.create_subprocess_exec(
sys.executable,
sys.argv[0],
'backend',
str(port),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
port += 1
self.backend_processes.append(process)
print(f"Started backend server on port {(port-1)/port} with PID {process.pid}")
async def handle_client(self, reader, writer):
async with self.lock:
min_clients = min(self.client_counts)
server_index = self.client_counts.index(min_clients)
self.client_counts[server_index] += 1
backend = ('127.0.0.1', self.backend_ports[server_index])
try:
backend_reader, backend_writer = await asyncio.open_connection(*backend)
async def forward(r, w):
try:
while True:
data = await r.read(1024)
if not data:
break
w.write(data)
await w.drain()
except asyncio.CancelledError:
pass
finally:
w.close()
task1 = asyncio.create_task(forward(reader, backend_writer))
task2 = asyncio.create_task(forward(backend_reader, writer))
await asyncio.gather(task1, task2)
except Exception as e:
print(f"Error: {e}")
finally:
writer.close()
async with self.lock:
self.client_counts[server_index] -= 1
async def monitor(self):
while True:
await asyncio.sleep(5)
print("Connected clients per server:")
for i, count in enumerate(self.client_counts):
print(f"Server {self.backend_ports[i]}: {count} clients")
async def start(self, host='0.0.0.0', port=8081,workers=5):
await self.start_backend_servers(port,workers)
server = await asyncio.start_server(self.handle_client, host, port)
monitor_task = asyncio.create_task(self.monitor())
# Handle shutdown gracefully
try:
async with server:
await server.serve_forever()
except asyncio.CancelledError:
pass
finally:
# Terminate backend processes
for process in self.backend_processes:
process.terminate()
await asyncio.gather(*(p.wait() for p in self.backend_processes))
print("Backend processes terminated.")
async def backend_echo_server(port):
async def handle_echo(reader, writer):
try:
while True:
data = await reader.read(1024)
if not data:
break
writer.write(data)
await writer.drain()
except Exception:
pass
finally:
writer.close()
server = await asyncio.start_server(handle_echo, '127.0.0.1', port)
print(f"Backend echo server running on port {port}")
await server.serve_forever()
async def main():
backend_ports = [8001, 8003, 8005, 8006]
# Launch backend echo servers
# Wait a moment for servers to start
lb = LoadBalancer(backend_ports)
await lb.start()
if __name__ == "__main__":
if len(sys.argv) > 1:
if sys.argv[1] == 'backend':
port = int(sys.argv[2])
from snek.app import Application
snek = Application(port=port)
web.run_app(snek, port=port, host='127.0.0.1')
elif sys.argv[1] == 'sync':
from snek.sync import app
web.run_app(snek, port=port, host='127.0.0.1')
else:
try:
asyncio.run(main())
except KeyboardInterrupt:
print("Shutting down...")
+38 -9
View File
@@ -1,14 +1,43 @@
import pathlib import pathlib
from aiohttp import web from aiohttp import web
from app.app import Application as BaseApplication from app.app import Application as BaseApplication
from snek.system.markdown import MarkdownExtension from snek.system.markdown import MarkdownExtension
class Application(BaseApplication): class Application(BaseApplication):
def __init__(A,path=None,*B,**C):A.path=pathlib.Path(path);D=A.path;super().__init__(*B,template_path=D,**C);A.jinja2_env.add_extension(MarkdownExtension);A.router.add_get('/{tail:.*}',A.handle_document)
async def handle_document(B,request): def __init__(self, path=None, *args, **kwargs):
D='text/plain';E=b'Resource is not found on this server.';F='index.html';G=request;C=G.match_info['tail'].strip('/') self.path = pathlib.Path(path)
if C=='':C=F template_path = self.path
A=B.path.joinpath(C)
if not A.exists():return web.Response(status=404,body=E,content_type=D) super().__init__(template_path=template_path, *args, **kwargs)
if A.is_dir():A=A.joinpath(F) self.jinja2_env.add_extension(MarkdownExtension)
if not A.exists():return web.Response(status=404,body=E,content_type=D)
H=await B.render_template(str(A.relative_to(B.path)),G);return H self.router.add_get("/{tail:.*}", self.handle_document)
async def handle_document(self, request):
relative_path = request.match_info["tail"].strip("/")
if relative_path == "":
relative_path = "index.html"
document_path = self.path.joinpath(relative_path)
if not document_path.exists():
return web.Response(
status=404,
body=b"Resource is not found on this server.",
content_type="text/plain",
)
if document_path.is_dir():
document_path = document_path.joinpath("index.html")
if not document_path.exists():
return web.Response(
status=404,
body=b"Resource is not found on this server.",
content_type="text/plain",
)
response = await self.render_template(
str(document_path.relative_to(self.path)), request
)
return response
+36 -9
View File
@@ -1,12 +1,39 @@
_B='created_at'
_A='uid'
import asyncio import asyncio
from snek.app import app from snek.app import app
async def fix_message(message):C='user';D='text';B='user_uid';A=message;A={_A:A[_A],B:A[B],D:A['message'],'sent':A[_B]};E=await app.services.user.get(uid=A[B]);A[C]=E and E['username']or None;return(A[C]or'')+': '+(A[D]or'')
async def fix_message(message):
message = {
"uid": message["uid"],
"user_uid": message["user_uid"],
"text": message["message"],
"sent": message["created_at"],
}
user = await app.services.user.get(uid=message["user_uid"])
message["user"] = user and user["username"] or None
return (message["user"] or "") + ": " + (message["text"] or "")
async def dump_public_channels(): async def dump_public_channels():
A=[] result = []
for B in app.db['channel'].find(is_private=False,is_listed=True,tag='public'):print(f"Dumping channel: {B["label"]}.");A+=[await fix_message(A)for A in app.db['channel_message'].find(channel_uid=B[_A],order_by=_B)];print('Dump succesfull!') for channel in app.db["channel"].find(
print('Converting to json.');print('Converting succesful, now writing to dump.json') is_private=False, is_listed=True, tag="public"
with open('dump.txt','w')as C:C.write('\n\n'.join(A)) ):
print('Dump written to dump.json') print(f"Dumping channel: {channel['label']}.")
if __name__=='__main__':asyncio.run(dump_public_channels()) result += [
await fix_message(record)
for record in app.db["channel_message"].find(
channel_uid=channel["uid"], order_by="created_at"
)
]
print("Dump succesfull!")
print("Converting to json.")
print("Converting succesful, now writing to dump.json")
with open("dump.txt", "w") as f:
f.write("\n\n".join(result))
print("Dump written to dump.json")
if __name__ == "__main__":
asyncio.run(dump_public_channels())
+49 -12
View File
@@ -1,14 +1,51 @@
_B='username' from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
_A='password'
from snek.system.form import Form,FormButtonElement,FormInputElement,HTMLElement
class AuthField(FormInputElement): class AuthField(FormInputElement):
@property
async def errors(self): @property
A=self;B=await super().errors async def errors(self):
if A.model.password.value and A.model.username.value: result = await super().errors
if not await A.app.services.user.validate_login(A.model.username.value,A.model.password.value):return['Invalid username or password'] if self.model.password.value and self.model.username.value:
return B if not await self.app.services.user.validate_login(
self.model.username.value, self.model.password.value
):
return ["Invalid username or password"]
return result
class LoginForm(Form): class LoginForm(Form):
title=HTMLElement(tag='h1',text='Login');username=AuthField(name=_B,required=True,min_length=2,max_length=20,regex='^[a-zA-Z0-9_-]+$',place_holder='Username',type='text');password=AuthField(name=_A,required=True,min_length=1,type=_A,place_holder='Password');action=FormButtonElement(name='action',value='submit',text='Login',type='button')
@property title = HTMLElement(tag="h1", text="Login")
async def is_valid(self):A=self;return all([A[_B],A[_A],not await A.username.errors,not await A.password.errors])
username = AuthField(
name="username",
required=True,
min_length=2,
max_length=20,
regex=r"^[a-zA-Z0-9_-]+$",
place_holder="Username",
type="text",
)
password = AuthField(
name="password",
required=True,
min_length=1,
type="password",
place_holder="Password",
)
action = FormButtonElement(
name="action", value="submit", text="Login", type="button"
)
@property
async def is_valid(self):
return all(
[
self["username"],
self["password"],
not await self.username.errors,
not await self.password.errors,
]
)
+43 -9
View File
@@ -1,10 +1,44 @@
_B='password' from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
_A='Register'
from snek.system.form import Form,FormButtonElement,FormInputElement,HTMLElement
class UsernameField(FormInputElement): class UsernameField(FormInputElement):
@property
async def errors(self): @property
A=self;B=await super().errors async def errors(self):
if A.value and await A.app.services.user.count(username=A.value):B.append('Username is not available.') result = await super().errors
return B if self.value and await self.app.services.user.count(username=self.value):
class RegisterForm(Form):title=HTMLElement(tag='h1',text=_A);username=UsernameField(name='username',required=True,min_length=2,max_length=20,regex='^[a-zA-Z0-9_-]+$',place_holder='Username',type='text');email=FormInputElement(name='email',required=False,regex='^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$',place_holder='Email address',type='email');password=FormInputElement(name=_B,required=True,min_length=1,type=_B,place_holder='Password');action=FormButtonElement(name='action',value='submit',text=_A,type='button') result.append("Username is not available.")
return result
class RegisterForm(Form):
title = HTMLElement(tag="h1", text="Register")
username = UsernameField(
name="username",
required=True,
min_length=2,
max_length=20,
regex=r"^[a-zA-Z0-9_-]+$",
place_holder="Username",
type="text",
)
email = FormInputElement(
name="email",
required=False,
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
place_holder="Email address",
type="email",
)
password = FormInputElement(
name="password",
required=True,
min_length=1,
type="password",
place_holder="Password",
)
action = FormButtonElement(
name="action", value="submit", text="Register", type="button"
)
+18 -2
View File
@@ -1,2 +1,18 @@
from snek.system.form import Form,FormButtonElement,FormInputElement,HTMLElement from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
class SearchUserForm(Form):title=HTMLElement(tag='h1',text='Search user');username=FormInputElement(name='username',required=True,min_length=1,max_length=128,place_holder='Username');action=FormButtonElement(name='action',value='submit',text='Search',type='button')
class SearchUserForm(Form):
title = HTMLElement(tag="h1", text="Search user")
username = FormInputElement(
name="username",
required=True,
min_length=1,
max_length=128,
place_holder="Username",
)
action = FormButtonElement(
name="action", value="submit", text="Search", type="button"
)
+25 -5
View File
@@ -1,5 +1,25 @@
_C='button' from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
_B='submit'
_A='action'
from snek.system.form import Form,FormButtonElement,FormInputElement,HTMLElement class SettingsProfileForm(Form):
class SettingsProfileForm(Form):nick=FormInputElement(name='nick',required=True,place_holder='Your Nickname',min_length=1,max_length=20);action=FormButtonElement(name=_A,value=_B,text='Save',type=_C);title=HTMLElement(tag='h1',text='Profile');profile=FormInputElement(name='profile',place_holder='Tell about yourself.',required=False,max_length=300);action=FormButtonElement(name=_A,value=_B,text='Save',type=_C)
nick = FormInputElement(
name="nick",
required=True,
place_holder="Your Nickname",
min_length=1,
max_length=20,
)
action = FormButtonElement(
name="action", value="submit", text="Save", type="button"
)
title = HTMLElement(tag="h1", text="Profile")
profile = FormInputElement(
name="profile",
place_holder="Tell about yourself.",
required=False,
max_length=300,
)
action = FormButtonElement(
name="action", value="submit", text="Save", type="button"
)
+2 -1
View File
@@ -1,2 +1,3 @@
from snek.app import app from snek.app import app
application=app
application = app
+26 -2
View File
@@ -1,4 +1,5 @@
import functools import functools
from snek.mapper.channel import ChannelMapper from snek.mapper.channel import ChannelMapper
from snek.mapper.channel_member import ChannelMemberMapper from snek.mapper.channel_member import ChannelMemberMapper
from snek.mapper.channel_message import ChannelMessageMapper from snek.mapper.channel_message import ChannelMessageMapper
@@ -8,7 +9,30 @@ from snek.mapper.notification import NotificationMapper
from snek.mapper.user import UserMapper from snek.mapper.user import UserMapper
from snek.mapper.user_property import UserPropertyMapper from snek.mapper.user_property import UserPropertyMapper
from snek.mapper.repository import RepositoryMapper from snek.mapper.repository import RepositoryMapper
from snek.mapper.channel_attachment import ChannelAttachmentMapper
from snek.mapper.container import ContainerMapper
from snek.system.object import Object from snek.system.object import Object
@functools.cache @functools.cache
def get_mappers(app=None):A=app;return Object(**{'user':UserMapper(app=A),'channel_member':ChannelMemberMapper(app=A),'channel':ChannelMapper(app=A),'channel_message':ChannelMessageMapper(app=A),'notification':NotificationMapper(app=A),'drive_item':DriveItemMapper(app=A),'drive':DriveMapper(app=A),'user_property':UserPropertyMapper(app=A),'repository':RepositoryMapper(app=A)})
def get_mapper(name,app=None):return get_mappers(app=app)[name] def get_mappers(app=None):
return Object(
**{
"user": UserMapper(app=app),
"channel_member": ChannelMemberMapper(app=app),
"channel": ChannelMapper(app=app),
"channel_message": ChannelMessageMapper(app=app),
"notification": NotificationMapper(app=app),
"drive_item": DriveItemMapper(app=app),
"drive": DriveMapper(app=app),
"user_property": UserPropertyMapper(app=app),
"repository": RepositoryMapper(app=app),
"channel_attachment": ChannelAttachmentMapper(app=app),
"container": ContainerMapper(app=app),
}
)
def get_mapper(name, app=None):
return get_mappers(app=app)[name]
+5 -1
View File
@@ -1,3 +1,7 @@
from snek.model.channel import ChannelModel from snek.model.channel import ChannelModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper
class ChannelMapper(BaseMapper):table_name='channel';model_class=ChannelModel
class ChannelMapper(BaseMapper):
table_name = "channel"
model_class = ChannelModel
+7
View File
@@ -0,0 +1,7 @@
from snek.model.channel_attachment import ChannelAttachmentModel
from snek.system.mapper import BaseMapper
class ChannelAttachmentMapper(BaseMapper):
table_name = "channel_attachment"
model_class = ChannelAttachmentModel
+5 -1
View File
@@ -1,3 +1,7 @@
from snek.model.channel_member import ChannelMemberModel from snek.model.channel_member import ChannelMemberModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper
class ChannelMemberMapper(BaseMapper):table_name='channel_member';model_class=ChannelMemberModel
class ChannelMemberMapper(BaseMapper):
table_name = "channel_member"
model_class = ChannelMemberModel
+5 -1
View File
@@ -1,3 +1,7 @@
from snek.model.channel_message import ChannelMessageModel from snek.model.channel_message import ChannelMessageModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper
class ChannelMessageMapper(BaseMapper):model_class=ChannelMessageModel;table_name='channel_message'
class ChannelMessageMapper(BaseMapper):
model_class = ChannelMessageModel
table_name = "channel_message"
+6
View File
@@ -0,0 +1,6 @@
from snek.model.container import Container
from snek.system.mapper import BaseMapper
class ContainerMapper(BaseMapper):
model_class = Container
table_name = "container"
+5 -1
View File
@@ -1,3 +1,7 @@
from snek.model.drive import DriveModel from snek.model.drive import DriveModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper
class DriveMapper(BaseMapper):table_name='drive';model_class=DriveModel
class DriveMapper(BaseMapper):
table_name = "drive"
model_class = DriveModel
+6 -1
View File
@@ -1,3 +1,8 @@
from snek.model.drive_item import DriveItemModel from snek.model.drive_item import DriveItemModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper
class DriveItemMapper(BaseMapper):model_class=DriveItemModel;table_name='drive_item'
class DriveItemMapper(BaseMapper):
model_class = DriveItemModel
table_name = "drive_item"
+5 -1
View File
@@ -1,3 +1,7 @@
from snek.model.notification import NotificationModel from snek.model.notification import NotificationModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper
class NotificationMapper(BaseMapper):table_name='notification';model_class=NotificationModel
class NotificationMapper(BaseMapper):
table_name = "notification"
model_class = NotificationModel
+5 -1
View File
@@ -1,3 +1,7 @@
from snek.model.repository import RepositoryModel from snek.model.repository import RepositoryModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper
class RepositoryMapper(BaseMapper):model_class=RepositoryModel;table_name='repository'
class RepositoryMapper(BaseMapper):
model_class = RepositoryModel
table_name = "repository"
+17 -4
View File
@@ -1,7 +1,20 @@
from snek.model.user import UserModel from snek.model.user import UserModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper
class UserMapper(BaseMapper): class UserMapper(BaseMapper):
table_name='user';model_class=UserModel table_name = "user"
def get_admin_uids(A): model_class = UserModel
try:return[A['uid']for A in A.db.query('SELECT uid FROM user WHERE is_admin = :is_admin',{'is_admin':True})]
except Exception as B:print(B);return[] def get_admin_uids(self):
try:
return [
user["uid"]
for user in self.db.query(
"SELECT uid FROM user WHERE is_admin = :is_admin",
{"is_admin": True},
)
]
except Exception as ex:
print(ex)
return []
+5 -1
View File
@@ -1,3 +1,7 @@
from snek.model.user_property import UserPropertyModel from snek.model.user_property import UserPropertyModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper
class UserPropertyMapper(BaseMapper):table_name='user_property';model_class=UserPropertyModel
class UserPropertyMapper(BaseMapper):
table_name = "user_property"
model_class = UserPropertyModel
+27 -2
View File
@@ -1,6 +1,9 @@
import functools import functools
from snek.model.channel import ChannelModel from snek.model.channel import ChannelModel
from snek.model.channel_member import ChannelMemberModel from snek.model.channel_member import ChannelMemberModel
# from snek.model.channel_message import ChannelMessageModel
from snek.model.channel_message import ChannelMessageModel from snek.model.channel_message import ChannelMessageModel
from snek.model.drive import DriveModel from snek.model.drive import DriveModel
from snek.model.drive_item import DriveItemModel from snek.model.drive_item import DriveItemModel
@@ -8,7 +11,29 @@ from snek.model.notification import NotificationModel
from snek.model.user import UserModel from snek.model.user import UserModel
from snek.model.user_property import UserPropertyModel from snek.model.user_property import UserPropertyModel
from snek.model.repository import RepositoryModel from snek.model.repository import RepositoryModel
from snek.model.channel_attachment import ChannelAttachmentModel
from snek.model.container import Container
from snek.system.object import Object from snek.system.object import Object
@functools.cache @functools.cache
def get_models():return Object(**{'user':UserModel,'channel_member':ChannelMemberModel,'channel':ChannelModel,'channel_message':ChannelMessageModel,'drive_item':DriveItemModel,'drive':DriveModel,'notification':NotificationModel,'user_property':UserPropertyModel,'repository':RepositoryModel}) def get_models():
def get_model(name):return get_models()[name] return Object(
**{
"user": UserModel,
"channel_member": ChannelMemberModel,
"channel": ChannelModel,
"channel_message": ChannelMessageModel,
"drive_item": DriveItemModel,
"drive": DriveModel,
"notification": NotificationModel,
"user_property": UserPropertyModel,
"repository": RepositoryModel,
"channel_attachment": ChannelAttachmentModel,
"container": Container,
}
)
def get_model(name):
return get_models()[name]
+28 -10
View File
@@ -1,12 +1,30 @@
_C='uid'
_B=False
_A=True
from snek.model.channel_message import ChannelMessageModel from snek.model.channel_message import ChannelMessageModel
from snek.system.model import BaseModel,ModelField from snek.system.model import BaseModel, ModelField
class ChannelModel(BaseModel): class ChannelModel(BaseModel):
label=ModelField(name='label',required=_A,kind=str);description=ModelField(name='description',required=_B,kind=str);tag=ModelField(name='tag',required=_B,kind=str);created_by_uid=ModelField(name='created_by_uid',required=_A,kind=str);is_private=ModelField(name='is_private',required=_A,kind=bool,value=_B);is_listed=ModelField(name='is_listed',required=_A,kind=bool,value=_A);index=ModelField(name='index',required=_A,kind=int,value=1000);last_message_on=ModelField(name='last_message_on',required=_B,kind=str) label = ModelField(name="label", required=True, kind=str)
async def get_last_message(A): description = ModelField(name="description", required=False, kind=str)
try: tag = ModelField(name="tag", required=False, kind=str)
async for B in A.app.services.channel_message.query('SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1',{'channel_uid':A[_C]}):return await A.app.services.channel_message.get(uid=B[_C]) created_by_uid = ModelField(name="created_by_uid", required=True, kind=str)
except:pass is_private = ModelField(name="is_private", required=True, kind=bool, value=False)
async def get_members(A):return await A.app.services.channel_member.find(channel_uid=A[_C],deleted_at=None,is_banned=_B) is_listed = ModelField(name="is_listed", required=True, kind=bool, value=True)
index = ModelField(name="index", required=True, kind=int, value=1000)
last_message_on = ModelField(name="last_message_on", required=False, kind=str)
async def get_last_message(self) -> ChannelMessageModel:
try:
async for model in self.app.services.channel_message.query(
"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1",
{"channel_uid": self["uid"]},
):
return await self.app.services.channel_message.get(uid=model["uid"])
except:
pass
return None
async def get_members(self):
return await self.app.services.channel_member.find(
channel_uid=self["uid"], deleted_at=None, is_banned=False
)
+16
View File
@@ -0,0 +1,16 @@
from snek.system.model import BaseModel
from snek.system.model import BaseModel, ModelField
class ChannelAttachmentModel(BaseModel):
name = ModelField(name="name", required=True, kind=str)
channel_uid = ModelField(name="channel_uid", required=True, kind=str)
path = ModelField(name="path", required=True, kind=str)
size = ModelField(name="size", required=False, kind=int)
user_uid = ModelField(name="user_uid", required=True, kind=str)
mime_type = ModelField(name="type", required=True, kind=str)
relative_url = ModelField(name="relative_url", required=True, kind=str)
resource_type = ModelField(name="resource_type", required=True, kind=str,value="file")
+40 -18
View File
@@ -1,19 +1,41 @@
_D='channel_uid' from snek.system.model import BaseModel, ModelField
_C='user_uid'
_B=False
_A=True
from snek.system.model import BaseModel,ModelField
class ChannelMemberModel(BaseModel): class ChannelMemberModel(BaseModel):
label=ModelField(name='label',required=_A,kind=str);channel_uid=ModelField(name=_D,required=_A,kind=str);user_uid=ModelField(name=_C,required=_A,kind=str);is_moderator=ModelField(name='is_moderator',required=_A,kind=bool,value=_B);is_read_only=ModelField(name='is_read_only',required=_A,kind=bool,value=_B);is_muted=ModelField(name='is_muted',required=_A,kind=bool,value=_B);is_banned=ModelField(name='is_banned',required=_A,kind=bool,value=_B);new_count=ModelField(name='new_count',required=_B,kind=int,value=0) label = ModelField(name="label", required=True, kind=str)
async def get_user(A):return await A.app.services.user.get(uid=A[_C]) channel_uid = ModelField(name="channel_uid", required=True, kind=str)
async def get_channel(A):return await A.app.services.channel.get(uid=A[_D]) user_uid = ModelField(name="user_uid", required=True, kind=str)
async def get_name(A): is_moderator = ModelField(
B=await A.get_channel() name="is_moderator", required=True, kind=bool, value=False
if B['tag']=='dm':C=await A.get_other_dm_user();return C['nick'] )
return B['name']or A['label'] is_read_only = ModelField(
async def get_other_dm_user(A): name="is_read_only", required=True, kind=bool, value=False
B='uid';C=await A.get_channel() )
if C['tag']!='dm':return is_muted = ModelField(name="is_muted", required=True, kind=bool, value=False)
async for D in A.app.services.channel_member.find(channel_uid=C[B]): is_banned = ModelField(name="is_banned", required=True, kind=bool, value=False)
if D[B]!=A[B]:return await A.app.services.user.get(uid=D[_C]) new_count = ModelField(name="new_count", required=False, kind=int, value=0)
return await A.get_user()
async def get_user(self):
return await self.app.services.user.get(uid=self["user_uid"])
async def get_channel(self):
return await self.app.services.channel.get(uid=self["channel_uid"])
async def get_name(self):
channel = await self.get_channel()
if channel["tag"] == "dm":
user = await self.get_other_dm_user()
return user["nick"]
return channel["name"] or self["label"]
async def get_other_dm_user(self):
channel = await self.get_channel()
if channel["tag"] != "dm":
return None
async for model in self.app.services.channel_member.find(
channel_uid=channel["uid"]
):
if model["uid"] != self["uid"]:
return await self.app.services.user.get(uid=model["user_uid"])
return await self.get_user()
+17 -6
View File
@@ -1,8 +1,19 @@
_B='user_uid'
_A='channel_uid'
from snek.model.user import UserModel from snek.model.user import UserModel
from snek.system.model import BaseModel,ModelField from snek.system.model import BaseModel, ModelField
from datetime import datetime,timezone
class ChannelMessageModel(BaseModel): class ChannelMessageModel(BaseModel):
channel_uid=ModelField(name=_A,required=True,kind=str);user_uid=ModelField(name=_B,required=True,kind=str);message=ModelField(name='message',required=True,kind=str);html=ModelField(name='html',required=False,kind=str) channel_uid = ModelField(name="channel_uid", required=True, kind=str)
async def get_user(A):return await A.app.services.user.get(uid=A[_B]) user_uid = ModelField(name="user_uid", required=True, kind=str)
async def get_channel(A):return await A.app.services.channel.get(uid=A[_A]) message = ModelField(name="message", required=True, kind=str)
html = ModelField(name="html", required=False, kind=str)
is_final = ModelField(name="is_final", required=True, kind=bool, value=True)
def get_seconds_since_last_update(self):
return int((datetime.now(timezone.utc) - datetime.fromisoformat(self["updated_at"])).total_seconds())
async def get_user(self) -> UserModel:
return await self.app.services.user.get(uid=self["user_uid"])
async def get_channel(self):
return await self.app.services.channel.get(uid=self["channel_uid"])
+10
View File
@@ -0,0 +1,10 @@
from snek.system.model import BaseModel, ModelField
class Container(BaseModel):
id = ModelField(name="id", required=True, kind=str)
name = ModelField(name="name", required=True, kind=str)
status = ModelField(name="status", required=True, kind=str)
resources = ModelField(name="resources", required=False, kind=str)
user_uid = ModelField(name="user_uid", required=False, kind=str)
path = ModelField(name="path", required=False, kind=str)
readonly = ModelField(name="readonly", required=False, kind=bool, default=False)
+13 -5
View File
@@ -1,6 +1,14 @@
from snek.system.model import BaseModel,ModelField from snek.system.model import BaseModel, ModelField
class DriveModel(BaseModel): class DriveModel(BaseModel):
user_uid=ModelField(name='user_uid',required=True);name=ModelField(name='name',required=False,type=str)
@property user_uid = ModelField(name="user_uid", required=True)
async def items(self): name = ModelField(name="name", required=False, type=str)
async for A in self.app.services.drive_item.find(drive_uid=self['uid']):yield A
@property
async def items(self):
async for drive_item in self.app.services.drive_item.find(
drive_uid=self["uid"]
):
yield drive_item
+19 -8
View File
@@ -1,10 +1,21 @@
_B='name'
_A=True
import mimetypes import mimetypes
from snek.system.model import BaseModel,ModelField
from snek.system.model import BaseModel, ModelField
class DriveItemModel(BaseModel): class DriveItemModel(BaseModel):
drive_uid=ModelField(name='drive_uid',required=_A,kind=str);name=ModelField(name=_B,required=_A,kind=str);path=ModelField(name='path',required=_A,kind=str);file_type=ModelField(name='file_type',required=_A,kind=str);file_size=ModelField(name='file_size',required=_A,kind=int);is_available=ModelField(name='is_available',required=_A,kind=bool,initial_value=_A) drive_uid = ModelField(name="drive_uid", required=True, kind=str)
@property name = ModelField(name="name", required=True, kind=str)
def extension(self):return self[_B].split('.')[-1] path = ModelField(name="path", required=True, kind=str)
@property file_type = ModelField(name="file_type", required=True, kind=str)
def mime_type(self):A,B=mimetypes.guess_type(self[_B]);return A file_size = ModelField(name="file_size", required=True, kind=int)
is_available = ModelField(name="is_available", required=True, kind=bool, initial_value=True)
@property
def extension(self):
return self["name"].split(".")[-1]
@property
def mime_type(self):
mimetype, _ = mimetypes.guess_type(self["name"])
return mimetype
+9 -3
View File
@@ -1,3 +1,9 @@
_A=True from snek.system.model import BaseModel, ModelField
from snek.system.model import BaseModel,ModelField
class NotificationModel(BaseModel):object_uid=ModelField(name='object_uid',required=_A);object_type=ModelField(name='object_type',required=_A);message=ModelField(name='message',required=_A);user_uid=ModelField(name='user_uid',required=_A);read_at=ModelField(name='is_read',required=_A)
class NotificationModel(BaseModel):
object_uid = ModelField(name="object_uid", required=True)
object_type = ModelField(name="object_type", required=True)
message = ModelField(name="message", required=True)
user_uid = ModelField(name="user_uid", required=True)
read_at = ModelField(name="is_read", required=True)
+13 -2
View File
@@ -1,3 +1,14 @@
from snek.model.user import UserModel from snek.model.user import UserModel
from snek.system.model import BaseModel,ModelField from snek.system.model import BaseModel, ModelField
class RepositoryModel(BaseModel):user_uid=ModelField(name='user_uid',required=True,kind=str);name=ModelField(name='name',required=True,kind=str);is_private=ModelField(name='is_private',required=False,kind=bool)
class RepositoryModel(BaseModel):
user_uid = ModelField(name="user_uid", required=True, kind=str)
name = ModelField(name="name", required=True, kind=str)
is_private = ModelField(name="is_private", required=False, kind=bool)
+59 -16
View File
@@ -1,17 +1,60 @@
_D='^[a-zA-Z0-9_-+/]+$' from snek.system.model import BaseModel, ModelField
_C=False
_B=True
_A='uid'
from snek.system.model import BaseModel,ModelField
class UserModel(BaseModel): class UserModel(BaseModel):
username=ModelField(name='username',required=_B,min_length=2,max_length=20,regex=_D);nick=ModelField(name='nick',required=_B,min_length=2,max_length=20,regex=_D);color=ModelField(name='color',required=_B,regex='^#[0-9a-fA-F]{6}$',kind=str);email=ModelField(name='email',required=_C,regex='^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$');password=ModelField(name='password',required=_B,min_length=1);last_ping=ModelField(name='last_ping',required=_C,kind=str);is_admin=ModelField(name='is_admin',required=_C,kind=bool)
async def get_property(A,name): username = ModelField(
B=await A.app.services.user_property.find_one(user_uid=A[_A],name=name) name="username",
if B:return B['value'] required=True,
async def has_property(A,name):return await A.app.services.user_property.exists(user_uid=A[_A],name=name) min_length=2,
async def set_property(A,name,value): max_length=20,
C=value;B=name regex=r"^[a-zA-Z0-9_-+/]+$",
if not await A.has_property(B):await A.app.services.user_property.insert(user_uid=A[_A],name=B,value=C) )
else:await A.app.services.user_property.update(user_uid=A[_A],name=B,value=C) nick = ModelField(
async def get_channel_members(A): name="nick",
async for B in A.app.services.channel_member.find(user_uid=A[_A],is_banned=_C,deleted_at=None):yield B required=True,
min_length=2,
max_length=20,
regex=r"^[a-zA-Z0-9_-+/]+$",
)
color = ModelField(
name="color", required=True, regex=r"^#[0-9a-fA-F]{6}$", kind=str
)
email = ModelField(
name="email",
required=False,
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
)
password = ModelField(name="password", required=True, min_length=1)
last_ping = ModelField(name="last_ping", required=False, kind=str)
is_admin = ModelField(name="is_admin", required=False, kind=bool)
async def get_property(self, name):
prop = await self.app.services.user_property.find_one(
user_uid=self["uid"], name=name
)
if prop:
return prop["value"]
async def has_property(self, name):
return await self.app.services.user_property.exists(
user_uid=self["uid"], name=name
)
async def set_property(self, name, value):
if not await self.has_property(name):
await self.app.services.user_property.insert(
user_uid=self["uid"], name=name, value=value
)
else:
await self.app.services.user_property.update(
user_uid=self["uid"], name=name, value=value
)
async def get_channel_members(self):
async for channel_member in self.app.services.channel_member.find(
user_uid=self["uid"], is_banned=False, deleted_at=None
):
yield channel_member
+7 -2
View File
@@ -1,2 +1,7 @@
from snek.system.model import BaseModel,ModelField from snek.system.model import BaseModel, ModelField
class UserPropertyModel(BaseModel):user_uid=ModelField(name='user_uid',required=True,kind=str);name=ModelField(name='name',required=True,kind=str);value=ModelField(name='path',required=True,kind=str)
class UserPropertyModel(BaseModel):
user_uid = ModelField(name="user_uid", required=True, kind=str)
name = ModelField(name="name", required=True, kind=str)
value = ModelField(name="path", required=True, kind=str)
+242
View File
@@ -0,0 +1,242 @@
import json
import asyncio
import aiohttp
from aiohttp import web
import dataset
import dataset.util
import traceback
import socket
import base64
import uuid
class DatasetMethod:
def __init__(self, dt, name):
self.dt = dt
self.name = name
def __call__(self, *args, **kwargs):
return self.dt.ds.call(
self.dt.name,
self.name,
*args,
**kwargs
)
class DatasetTable:
def __init__(self, ds, name):
self.ds = ds
self.name = name
def __getattr__(self, name):
return DatasetMethod(self, name)
class WebSocketClient2:
def __init__(self, uri):
self.uri = uri
self.loop = asyncio.get_event_loop()
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)
else:
# If loop isn't running, connect synchronously
self.loop.run_until_complete(self._connect())
async def _connect(self):
self.websocket = await websockets.connect(self.uri)
# Start listening for messages
asyncio.create_task(self._receive_loop())
async def _receive_loop(self):
try:
async for message in self.websocket:
await self.receive_queue.put(message)
except Exception:
pass # Handle exceptions as needed
def send(self, message: str):
if self.loop.is_running():
# Schedule send in the existing loop
asyncio.run_coroutine_threadsafe(self.websocket.send(message), self.loop)
else:
# If loop isn't running, run directly
self.loop.run_until_complete(self.websocket.send(message))
def receive(self):
# Wait for a message synchronously
future = asyncio.run_coroutine_threadsafe(self.receive_queue.get(), self.loop)
return future.result()
def close(self):
if self.websocket:
if self.loop.is_running():
asyncio.run_coroutine_threadsafe(self.websocket.close(), self.loop)
else:
self.loop.run_until_complete(self.websocket.close())
import websockets
class DatasetWrapper(object):
def __init__(self):
self.ws = WebSocketClient()
def begin(self):
self.call(None, 'begin')
def commit(self):
self.call(None, 'commit')
def __getitem__(self, name):
return DatasetTable(self, name)
def query(self, *args, **kwargs):
return self.call(None, 'query', *args, **kwargs)
def call(self, table, method, *args, **kwargs):
payload = {"table": table, "method": method, "args": args, "kwargs": kwargs,"call_uid":None}
#if method in ['find','find_one']:
payload["call_uid"] = str(uuid.uuid4())
self.ws.write(json.dumps(payload))
if payload["call_uid"]:
response = self.ws.read()
return json.loads(response)['result']
return True
class DatasetWebSocketView:
def __init__(self):
self.ws = None
self.db = dataset.connect('sqlite:///snek.db')
self.setattr(self, "db", self.get)
self.setattr(self, "db", self.set)
)
super()
def format_result(self, result):
try:
return dict(result)
except:
pass
try:
return [dict(row) for row in result]
except:
pass
return result
async def send_str(self, msg):
return await self.ws.send_str(msg)
def get(self, key):
returnl loads(dict(self.db['_kv'].get(key=key)['value']))
def set(self, key, value):
return self.db['_kv'].upsert({'key': key, 'value': json.dumps(value)}, ['key'])
async def handle(self, request):
ws = web.WebSocketResponse()
await ws.prepare(request)
self.ws = ws
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
try:
data = json.loads(msg.data)
call_uid = data.get("call_uid")
method = data.get("method")
table_name = data.get("table")
args = data.get("args", {})
kwargs = data.get("kwargs", {})
function = getattr(self.db, method, None)
if table_name:
function = getattr(self.db[table_name], method, None)
print(method, table_name, args, kwargs,flush=True)
if function:
response = {}
try:
result = function(*args, **kwargs)
print(result)
response['result'] = self.format_result(result)
response["call_uid"] = call_uid
response["success"] = True
except Exception as e:
response["call_uid"] = call_uid
response["success"] = False
response["error"] = str(e)
response["traceback"] = traceback.format_exc()
if call_uid:
await self.send_str(json.dumps(response,default=str))
else:
await self.send_str(json.dumps({"status": "error", "error":"Method not found.","call_uid": call_uid}))
except Exception as e:
await self.send_str(json.dumps({"success": False,"call_uid": call_uid, "error": str(e), "error": str(e), "traceback": traceback.format_exc()},default=str))
elif msg.type == aiohttp.WSMsgType.ERROR:
print('ws connection closed with exception %s' % ws.exception())
return ws
app = web.Application()
view = DatasetWebSocketView()
app.router.add_get('/db', view.handle)
async def run_server():
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, 'localhost', 3131)
await site.start()
print("Server started at http://localhost:8080")
await asyncio.Event().wait()
async def client():
print("x")
d = DatasetWrapper()
print("y")
for x in range(100):
for x in range(100):
if d['test'].insert({"name": "test", "number":x}):
print(".",end="",flush=True)
print("")
print(d['test'].find_one(name="test", order_by="-number"))
print("DONE")
import time
async def main():
await run_server()
import sys
if __name__ == '__main__':
if sys.argv[1] == 'server':
asyncio.run(main())
if sys.argv[1] == 'client':
asyncio.run(client())
+51
View File
@@ -0,0 +1,51 @@
import snek.serpentarium
import time
from concurrent.futures import ProcessPoolExecutor
durations = []
def task1():
global durations
client = snek.serpentarium.DatasetWrapper()
start=time.time()
for x in range(1500):
client['a'].delete()
client['a'].insert({"foo": x})
client['a'].find(foo=x)
client['a'].find_one(foo=x)
client['a'].count()
#print(client['a'].find(foo=x) )
#print(client['a'].find_one(foo=x) )
#print(client['a'].count())
client.close()
duration1 = f"{time.time()-start}"
durations.append(duration1)
print(durations)
with ProcessPoolExecutor(max_workers=4) as executor:
tasks = [executor.submit(task1),
executor.submit(task1),
executor.submit(task1),
executor.submit(task1)
]
for task in tasks:
task.result()
import dataset
client = dataset.connect("sqlite:///snek.db")
start=time.time()
for x in range(1500):
client['a'].delete()
client['a'].insert({"foo": x})
print([dict(row) for row in client['a'].find(foo=x)])
print(dict(client['a'].find_one(foo=x) ))
print(client['a'].count())
duration2 = f"{time.time()-start}"
print(duration1,duration2)
+103
View File
@@ -0,0 +1,103 @@
CREATE TABLE IF NOT EXISTS http_access (
id INTEGER NOT NULL,
created TEXT,
path TEXT,
duration FLOAT,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS user (
id INTEGER NOT NULL,
color TEXT,
created_at TEXT,
deleted_at TEXT,
email TEXT,
is_admin TEXT,
last_ping TEXT,
nick TEXT,
password TEXT,
uid TEXT,
updated_at TEXT,
username TEXT,
PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS ix_user_e2577dd78b54fe28 ON user (uid);
CREATE TABLE IF NOT EXISTS channel (
id INTEGER NOT NULL,
created_at TEXT,
created_by_uid TEXT,
deleted_at TEXT,
description TEXT,
"index" BIGINT,
is_listed BOOLEAN,
is_private BOOLEAN,
label TEXT,
last_message_on TEXT,
tag TEXT,
uid TEXT,
updated_at TEXT,
PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS ix_channel_e2577dd78b54fe28 ON channel (uid);
CREATE TABLE IF NOT EXISTS channel_member (
id INTEGER NOT NULL,
channel_uid TEXT,
created_at TEXT,
deleted_at TEXT,
is_banned BOOLEAN,
is_moderator BOOLEAN,
is_muted BOOLEAN,
is_read_only BOOLEAN,
label TEXT,
new_count BIGINT,
uid TEXT,
updated_at TEXT,
user_uid TEXT,
PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS ix_channel_member_e2577dd78b54fe28 ON channel_member (uid);
CREATE TABLE IF NOT EXISTS broadcast (
id INTEGER NOT NULL,
channel_uid TEXT,
message TEXT,
created_at TEXT,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS channel_message (
id INTEGER NOT NULL,
channel_uid TEXT,
created_at TEXT,
deleted_at TEXT,
html TEXT,
message TEXT,
uid TEXT,
updated_at TEXT,
user_uid TEXT,
PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS ix_channel_message_e2577dd78b54fe28 ON channel_message (uid);
CREATE TABLE IF NOT EXISTS notification (
id INTEGER NOT NULL,
created_at TEXT,
deleted_at TEXT,
message TEXT,
object_type TEXT,
object_uid TEXT,
read_at TEXT,
uid TEXT,
updated_at TEXT,
user_uid TEXT,
PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS ix_notification_e2577dd78b54fe28 ON notification (uid);
CREATE TABLE IF NOT EXISTS repository (
id INTEGER NOT NULL,
created_at TEXT,
deleted_at TEXT,
is_private BIGINT,
name TEXT,
uid TEXT,
updated_at TEXT,
user_uid TEXT,
PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS ix_repository_e2577dd78b54fe28 ON repository (uid);
+29 -2
View File
@@ -1,4 +1,5 @@
import functools import functools
from snek.service.channel import ChannelService from snek.service.channel import ChannelService
from snek.service.channel_member import ChannelMemberService from snek.service.channel_member import ChannelMemberService
from snek.service.channel_message import ChannelMessageService from snek.service.channel_message import ChannelMessageService
@@ -11,7 +12,33 @@ from snek.service.user import UserService
from snek.service.user_property import UserPropertyService from snek.service.user_property import UserPropertyService
from snek.service.util import UtilService from snek.service.util import UtilService
from snek.service.repository import RepositoryService from snek.service.repository import RepositoryService
from snek.service.channel_attachment import ChannelAttachmentService
from snek.service.container import ContainerService
from snek.system.object import Object from snek.system.object import Object
from snek.service.db import DBService
@functools.cache @functools.cache
def get_services(app):A=app;return Object(**{'user':UserService(app=A),'channel_member':ChannelMemberService(app=A),'channel':ChannelService(app=A),'channel_message':ChannelMessageService(app=A),'chat':ChatService(app=A),'socket':SocketService(app=A),'notification':NotificationService(app=A),'util':UtilService(app=A),'drive':DriveService(app=A),'drive_item':DriveItemService(app=A),'user_property':UserPropertyService(app=A),'repository':RepositoryService(app=A)}) def get_services(app):
def get_service(name,app=None):return get_services(app=app)[name] return Object(
**{
"user": UserService(app=app),
"channel_member": ChannelMemberService(app=app),
"channel": ChannelService(app=app),
"channel_message": ChannelMessageService(app=app),
"chat": ChatService(app=app),
"socket": SocketService(app=app),
"notification": NotificationService(app=app),
"util": UtilService(app=app),
"drive": DriveService(app=app),
"drive_item": DriveItemService(app=app),
"user_property": UserPropertyService(app=app),
"repository": RepositoryService(app=app),
"db": DBService(app=app),
"channel_attachment": ChannelAttachmentService(app=app),
"container": ContainerService(app=app),
}
)
def get_service(name, app=None):
return get_services(app=app)[name]
+117 -45
View File
@@ -1,49 +1,121 @@
_F='channel_uid'
_E='public'
_D=True
_C='uid'
_B=None
_A=False
from datetime import datetime from datetime import datetime
from snek.system.model import now from snek.system.model import now
from snek.system.service import BaseService from snek.system.service import BaseService
import pathlib
class ChannelService(BaseService): class ChannelService(BaseService):
mapper_name='channel' mapper_name = "channel"
async def get(E,uid=_B,**A):
D='name';C=uid async def get_attachment_folder(self, channel_uid,ensure=False):
if C: path = pathlib.Path(f"./drive/{channel_uid}/attachments")
A[_C]=C;B=await super().get(**A) if ensure:
if B:return B path.mkdir(
del A[_C];A[D]=C;B=await super().get(**A) parents=True, exist_ok=True
if B:return B )
A[D]='#'+C;B=await super().get(**A) return path
if B:return B
return async def get(self, uid=None, **kwargs):
return await super().get(**A) if uid:
async def create(C,label,created_by_uid,description=_B,tag=_B,is_private=_A,is_listed=_D): kwargs["uid"] = uid
E=is_listed;D=tag;B=label result = await super().get(**kwargs)
if B[0]!='#'and E:B=f"#{B}" if result:
F=await C.count(deleted_at=_B) return result
if not D and not F:D=_E del kwargs["uid"]
A=await C.new();A['label']=B;A['description']=description;A['tag']=D;A['created_by_uid']=created_by_uid;A['is_private']=is_private;A['is_listed']=E kwargs["name"] = uid
if await C.save(A):return A result = await super().get(**kwargs)
raise Exception(f"Failed to create channel: {A.errors}.") if result:
async def get_dm(A,user1,user2): return result
C=user2;B=user1;D=await A.services.channel_member.get_dm(B,C) kwargs["name"] = "#" + uid
if D:return await A.get(uid=D[_F]) result = await super().get(**kwargs)
E=await A.create('DM',B,tag='dm');await A.services.channel_member.create_dm(E[_C],B,C);return E if result:
async def get_users(A,channel_uid): return result
async for C in A.services.channel_member.find(channel_uid=channel_uid,is_banned=_A,is_muted=_A,deleted_at=_B): return None
B=await A.services.user.get(uid=C['user_uid']) return await super().get(**kwargs)
if B:yield B
async def get_online_users(C,channel_uid): async def create(
B='last_ping' self,
async for A in C.get_users(channel_uid): label,
if not A[B]:continue created_by_uid,
if(datetime.fromisoformat(now())-datetime.fromisoformat(A[B])).total_seconds()<20:yield A description=None,
async def get_for_user(A,user_uid): tag=None,
async for B in A.services.channel_member.find(user_uid=user_uid,is_banned=_A,deleted_at=_B):C=await A.get(uid=B[_F]);yield C is_private=False,
async def ensure_public_channel(B,created_by_uid): is_listed=True,
C=created_by_uid;A=await B.get(is_listed=_D,tag=_E);D=_A ):
if not A:D=_D;A=await B.create(_E,created_by_uid=C,is_listed=_D,tag=_E) if label[0] != "#" and is_listed:
await B.app.services.channel_member.create(A[_C],C,is_moderator=D,is_read_only=_A,is_muted=_A,is_banned=_A);return A label = f"#{label}"
count = await self.count(deleted_at=None)
if not tag and not count:
tag = "public"
model = await self.new()
model["label"] = label
model["description"] = description
model["tag"] = tag
model["created_by_uid"] = created_by_uid
model["is_private"] = is_private
model["is_listed"] = is_listed
if await self.save(model):
return model
raise Exception(f"Failed to create channel: {model.errors}.")
async def get_dm(self, user1, user2):
channel_member = await self.services.channel_member.get_dm(user1, user2)
if channel_member:
return await self.get(uid=channel_member["channel_uid"])
channel = await self.create("DM", user1, tag="dm")
await self.services.channel_member.create_dm(channel["uid"], user1, user2)
return channel
async def get_recent_users(self, channel_uid):
async for user in self.query("SELECT user.uid, user.username,user.color,user.last_ping,user.nick FROM channel_member INNER JOIN user ON user.uid = channel_member.user_uid WHERE channel_uid=:channel_uid AND user.last_ping >= datetime('now', '-3 minutes') ORDER BY last_ping DESC LIMIT 30", {"channel_uid": channel_uid}):
yield user
async def get_users(self, channel_uid):
async for channel_member in self.services.channel_member.find(
channel_uid=channel_uid,
is_banned=False,
is_muted=False,
deleted_at=None,
):
user = await self.services.user.get(uid=channel_member["user_uid"])
if user:
yield user
async def get_online_users(self, channel_uid):
async for user in self.get_users(channel_uid):
if not user["last_ping"]:
continue
if (
datetime.fromisoformat(now())
- datetime.fromisoformat(user["last_ping"])
).total_seconds() < 180:
yield user
async def get_for_user(self, user_uid):
async for channel_member in self.services.channel_member.find(
user_uid=user_uid,
is_banned=False,
deleted_at=None,
):
channel = await self.get(uid=channel_member["channel_uid"])
yield channel
async def ensure_public_channel(self, created_by_uid):
model = await self.get(is_listed=True, tag="public")
is_moderator = False
if not model:
is_moderator = True
model = await self.create(
"public", created_by_uid=created_by_uid, is_listed=True, tag="public"
)
await self.app.services.channel_member.create(
model["uid"],
created_by_uid,
is_moderator=is_moderator,
is_read_only=False,
is_muted=False,
is_banned=False,
)
return model
+25
View File
@@ -0,0 +1,25 @@
from snek.system.service import BaseService
import urllib.parse
import pathlib
import mimetypes
import uuid
class ChannelAttachmentService(BaseService):
mapper_name="channel_attachment"
async def create_file(self, channel_uid, user_uid, name):
attachment = await self.new()
attachment["channel_uid"] = channel_uid
attachment['user_uid'] = user_uid
attachment["name"] = name
attachment["mime_type"] = mimetypes.guess_type(name)[0]
attachment['resource_type'] = "file"
real_file_name = f"{attachment['uid']}-{name}"
attachment["relative_url"] = (f"{attachment['uid']}-{name}")
attachment_folder = await self.services.channel.get_attachment_folder(channel_uid)
attachment_path = attachment_folder.joinpath(real_file_name)
attachment["path"] = str(attachment_path)
if await self.save(attachment):
return attachment
raise Exception(f"Failed to create channel attachment: {attachment.errors}.")
+72 -26
View File
@@ -1,28 +1,74 @@
_C='user_uid'
_B='channel_uid'
_A=False
from snek.system.service import BaseService from snek.system.service import BaseService
class ChannelMemberService(BaseService): class ChannelMemberService(BaseService):
mapper_name='channel_member'
async def mark_as_read(A,channel_uid,user_uid):B=await A.get(channel_uid=channel_uid,user_uid=user_uid);B['new_count']=0;return await A.save(B) mapper_name = "channel_member"
async def get_user_uids(A,channel_uid):
async for B in A.mapper.query('SELECT user_uid FROM channel_member WHERE channel_uid=:channel_uid',{_B:channel_uid}):yield B[_C] async def mark_as_read(self, channel_uid, user_uid):
async def create(B,channel_uid,user_uid,is_moderator=_A,is_read_only=_A,is_muted=_A,is_banned=_A): channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)
D='label';E='is_banned';F=user_uid;C=channel_uid;A=await B.get(channel_uid=C,user_uid=F) channel_member["new_count"] = 0
if A: return await self.save(channel_member)
if A[E]:return _A
return A async def get_user_uids(self, channel_uid):
A=await B.new();G=await B.services.channel.get(uid=C);A[D]=G[D];A[_B]=C;A[_C]=F;A['is_moderator']=is_moderator;A['is_read_only']=is_read_only;A['is_muted']=is_muted;A[E]=is_banned async for model in self.mapper.query(
if await B.save(A):return A "SELECT user_uid FROM channel_member WHERE channel_uid=:channel_uid",
raise Exception(f"Failed to create channel member: {A.errors}.") {"channel_uid": channel_uid},
async def get_dm(D,from_user,to_user): ):
E='to_user';F='from_user';A=to_user;B=from_user yield model["user_uid"]
async for C in D.query("SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') INNER JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = channel.uid AND channel_member2.user_uid = :to_user) WHERE channel_member.user_uid=:from_user ",{F:B,E:A}):return C
if not B==A:return async def create(
async for C in D.query("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 ",{F:B,E:A}):return C self,
async def get_other_dm_user(A,channel_uid,user_uid): channel_uid,
B='uid';C=channel_uid;D=await A.get(channel_uid=C,user_uid=user_uid);F=await A.services.channel.get(uid=D[_B]) user_uid,
if F['tag']!='dm':return is_moderator=False,
async for E in A.services.channel_member.find(channel_uid=C): is_read_only=False,
if E[B]!=D[B]:return await A.services.user.get(uid=E[_C]) is_muted=False,
async def create_dm(A,channel_uid,from_user_uid,to_user_uid):B=channel_uid;C=await A.create(B,from_user_uid);await A.create(B,to_user_uid);return C is_banned=False,
):
model = await self.get(channel_uid=channel_uid, user_uid=user_uid)
if model:
if model["is_banned"]:
return False
return model
model = await self.new()
channel = await self.services.channel.get(uid=channel_uid)
model["label"] = channel["label"]
model["channel_uid"] = channel_uid
model["user_uid"] = user_uid
model["is_moderator"] = is_moderator
model["is_read_only"] = is_read_only
model["is_muted"] = is_muted
model["is_banned"] = is_banned
if await self.save(model):
return model
raise Exception(f"Failed to create channel member: {model.errors}.")
async def get_dm(self, from_user, to_user):
async for model in self.query(
"SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') INNER JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = channel.uid AND channel_member2.user_uid = :to_user) WHERE channel_member.user_uid=:from_user ",
{"from_user": from_user, "to_user": to_user},
):
return model
if not from_user == to_user:
return None
async for model in self.query(
"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):
channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)
channel = await self.services.channel.get(uid=channel_member["channel_uid"])
if channel["tag"] != "dm":
return None
async for model in self.services.channel_member.find(channel_uid=channel_uid):
if model["uid"] != channel_member["uid"]:
return await self.services.user.get(uid=model["user_uid"])
async def create_dm(self, channel_uid, from_user_uid, to_user_uid):
result = await self.create(channel_uid, from_user_uid)
await self.create(channel_uid, to_user_uid)
return result
+108 -31
View File
@@ -1,33 +1,110 @@
_I='user_nick'
_H='created_at'
_G='html'
_F='uid'
_E='message'
_D='color'
_C='username'
_B='user_uid'
_A='channel_uid'
from snek.system.service import BaseService from snek.system.service import BaseService
class ChannelMessageService(BaseService): class ChannelMessageService(BaseService):
mapper_name='channel_message' mapper_name = "channel_message"
async def create(B,channel_uid,user_uid,message):
E=user_uid;A=await B.new();A[_A]=channel_uid;A[_B]=E;A[_E]=message;D={};F=A.record;D.update(F);C=await B.app.services.user.get(uid=E);D.update({_B:C[_F],_C:C[_C],_I:C['nick'],_D:C[_D]}) async def create(self, channel_uid, user_uid, message, is_final=True):
try:G=B.app.jinja2_env.get_template('message.html');A[_G]=G.render(**D) model = await self.new()
except Exception as H:print(H,flush=True)
if await B.save(A):return A model["channel_uid"] = channel_uid
raise Exception(f"Failed to create channel message: {A.errors}.") model["user_uid"] = user_uid
async def to_extended_dict(C,message): model["message"] = message
A=message;B=await C.services.user.get(uid=A[_B]) model['is_final'] = is_final
if not B:return{}
return{_F:A[_F],_D:B[_D],_B:A[_B],_A:A[_A],_I:B['nick'],_E:A[_E],_H:A[_H],_G:A[_G],_C:B[_C]} context = {}
async def offset(D,channel_uid,page=0,timestamp=None,page_size=30):
J='timestamp';E='offset';F='page_size';G=timestamp;H=channel_uid;C=page_size;A=[];I=page*C record = model.record
try: context.update(record)
if G: user = await self.app.services.user.get(uid=user_uid)
async for B in D.query('SELECT * FROM channel_message WHERE channel_uid=:channel_uid AND created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size OFFSET :offset',{_A:H,F:C,E:I,J:G}):A.append(B) context.update(
elif page>0: {
async for B in D.query('SELECT * FROM channel_message WHERE channel_uid=:channel_uid WHERE created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size',{_A:H,F:C,E:I,J:G}):A.append(B) "user_uid": user["uid"],
else: "username": user["username"],
async for B in D.query('SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT :page_size OFFSET :offset',{_A:H,F:C,E:I}):A.append(B) "user_nick": user["nick"],
except:pass "color": user["color"],
A.sort(key=lambda x:x[_H]);return A }
)
try:
template = self.app.jinja2_env.get_template("message.html")
model["html"] = template.render(**context)
except Exception as ex:
print(ex, flush=True)
if await super().save(model):
return model
raise Exception(f"Failed to create channel message: {model.errors}.")
async def to_extended_dict(self, message):
user = await self.services.user.get(uid=message["user_uid"])
if not user:
return {}
return {
"uid": message["uid"],
"color": user["color"],
"user_uid": message["user_uid"],
"channel_uid": message["channel_uid"],
"user_nick": user["nick"],
"message": message["message"],
"created_at": message["created_at"],
"html": message["html"],
"username": user["username"],
}
async def save(self, model):
context = {}
context.update(model.record)
user = await self.app.services.user.get(model['user_uid'])
context.update(
{
"user_uid": user["uid"],
"username": user["username"],
"user_nick": user["nick"],
"color": user["color"],
}
)
template = self.app.jinja2_env.get_template("message.html")
model["html"] = template.render(**context)
return await super().save(model)
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
results = []
offset = page * page_size
try:
if timestamp:
async for model in self.query(
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid AND created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",
{
"channel_uid": channel_uid,
"page_size": page_size,
"offset": offset,
"timestamp": timestamp,
},
):
results.append(model)
elif page > 0:
async for model in self.query(
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid WHERE created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size",
{
"channel_uid": channel_uid,
"page_size": page_size,
"offset": offset,
"timestamp": timestamp,
},
):
results.append(model)
else:
async for model in self.query(
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",
{
"channel_uid": channel_uid,
"page_size": page_size,
"offset": offset,
},
):
results.append(model)
except:
pass
results.sort(key=lambda x: x["created_at"])
return results
+63 -4
View File
@@ -1,7 +1,66 @@
from snek.system.model import now from snek.system.model import now
from snek.system.service import BaseService from snek.system.service import BaseService
class ChatService(BaseService): class ChatService(BaseService):
async def send(A,user_uid,channel_uid,message):
H='username';I='created_at';J='color';K='html';L='message';D='uid';E=user_uid;C=channel_uid;F=await A.services.channel.get(uid=C) async def finalize(self, message_uid):
if not F:raise Exception('Channel not found.') channel_message = await self.services.channel_message.get(uid=message_uid)
B=await A.services.channel_message.create(C,E,message);M=B[D];G=await A.services.user.get(uid=E);F['last_message_on']=now();await A.services.channel.save(F);await A.services.socket.broadcast(C,{L:B[L],K:B[K],'user_uid':E,J:G[J],'channel_uid':C,I:B[I],'updated_at':None,H:G[H],D:B[D],'user_nick':G['nick']});await A.app.create_task(A.services.notification.create_channel_message(M));return True channel_message["is_final"] = True
await self.services.channel_message.save(channel_message)
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)
await self.services.socket.broadcast(
channel['uid'],
{
"message": channel_message["message"],
"html": channel_message["html"],
"user_uid": user['uid'],
"color": user["color"],
"channel_uid": channel['uid'],
"created_at": channel_message["created_at"],
"updated_at": channel_message["updated_at"],
"username": user["username"],
"uid": channel_message["uid"],
"user_nick": user["nick"],
"is_final": channel_message["is_final"],
},
)
async def send(self, user_uid, channel_uid, message, is_final=True):
channel = await self.services.channel.get(uid=channel_uid)
if not channel:
raise Exception("Channel not found.")
channel_message = await self.services.channel_message.create(
channel_uid, user_uid, message,is_final
)
channel_message_uid = channel_message["uid"]
user = await self.services.user.get(uid=user_uid)
channel["last_message_on"] = now()
await self.services.channel.save(channel)
await self.services.socket.broadcast(
channel_uid,
{
"message": channel_message["message"],
"html": channel_message["html"],
"user_uid": user_uid,
"color": user["color"],
"channel_uid": channel_uid,
"created_at": channel_message["created_at"],
"updated_at": None,
"username": user["username"],
"uid": channel_message["uid"],
"user_nick": user["nick"],
"is_final": is_final,
},
)
await self.app.create_task(
self.services.notification.create_channel_message(channel_message_uid)
)
return channel_message
+29
View File
@@ -0,0 +1,29 @@
from snek.system.service import BaseService
class ContainerService(BaseService):
mapper_name = "container"
async def create(self, id, name, status, resources=None, user_uid=None, path=None, readonly=False):
model = await self.new()
model["id"] = id
model["name"] = name
model["status"] = status
if resources:
model["resources"] = resources
if user_uid:
model["user_uid"] = user_uid
if path:
model["path"] = path
model["readonly"] = readonly
if await super().save(model):
return model
raise Exception(f"Failed to create container: {model.errors}")
async def get(self, id):
return await self.mapper.get(id)
async def update(self, model):
return await self.mapper.update(model)
async def delete(self, id):
return await self.mapper.delete(id)
+71
View File
@@ -0,0 +1,71 @@
from snek.system.service import BaseService
import dataset
import uuid
from datetime import datetime
class DBService(BaseService):
async def get_db(self, user_uid):
home_folder = await self.app.services.user.get_home_folder(user_uid)
home_folder.mkdir(parents=True, exist_ok=True)
db_path = home_folder.joinpath("snek/user.db")
db_path.parent.mkdir(parents=True, exist_ok=True)
return dataset.connect("sqlite:///" + str(db_path))
async def insert(self, user_uid, table_name, values):
db = await self.get_db(user_uid)
return db[table_name].insert(values)
async def update(self, user_uid, table_name, values, filters):
db = await self.get_db(user_uid)
if not filters:
filters = {}
if not values:
return False
return db[table_name].update(values, filters)
async def upsert(self, user_uid, table_name, values, keys):
db = await self.get_db(user_uid)
return db[table_name].upsert(values, keys)
async def find(self, user_uid, table_name, kwargs):
db = await self.get_db(user_uid)
kwargs['_limit'] = kwargs.get('_limit', 30)
return [dict(row) for row in db[table_name].find(**kwargs)]
async def get(self, user_uid, table_name, filters):
db = await self.get_db(user_uid)
if not filters:
filters = {}
try:
return dict(db[table_name].find_one(**filters))
except ValueError:
return None
async def delete(self, user_uid, table_name, filters):
db = await self.get_db(user_uid)
if not filters:
filters = {}
return db[table_name].delete(**filters)
async def query(self, sql,values):
db = await self.app.db
return [dict(row) for row in db.query(sql, values or {})]
async def exists(self, user_uid, table_name, filters):
db = await self.get_db(user_uid)
if not filters:
filters = {}
return bool(db[table_name].find_one(**filters))
async def count(self, user_uid, table_name, filters):
db = await self.get_db(user_uid)
if not filters:
filters = {}
return db[table_name].count(**filters)
+151 -39
View File
@@ -1,41 +1,153 @@
_H='Documents'
_G='Archives'
_F='Videos'
_E='Pictures'
_D='uid'
_C='user_uid'
_B='My Drive'
_A='name'
from snek.system.service import BaseService from snek.system.service import BaseService
class DriveService(BaseService): class DriveService(BaseService):
mapper_name='drive';EXTENSIONS_PICTURES=['jpg','jpeg','png','gif','svg','webp','tiff'];EXTENSIONS_VIDEOS=['mp4','m4v','mov','wmv','webm','mkv','mpg','mpeg','avi','ogv','ogg','flv','3gp','3g2'];EXTENSIONS_ARCHIVES=['zip','rar','7z','tar','tar.gz','tar.xz','tar.bz2','tar.lzma','tar.lz'];EXTENSIONS_AUDIO=['mp3','wav','ogg','flac','m4a','wma','aac','opus','aiff','au','mid','midi'];EXTENSIONS_DOCS=['pdf','doc','docx','xls','xlsx','ppt','pptx','txt','md','json','csv','xml','html','css','js','py','sql','rs','toml','yml','yaml','ini','conf','config','log','csv','tsv','java','cs','csproj','scss','less','sass','json','lock','lock.json','jsonl']
async def get_drive_name_by_extension(B,extension): mapper_name = "drive"
A=extension
if A.startswith('.'):A=A[1:] EXTENSIONS_PICTURES = ["jpg", "jpeg", "png", "gif", "svg", "webp", "tiff"]
if A in B.EXTENSIONS_PICTURES:return _E EXTENSIONS_VIDEOS = [
if A in B.EXTENSIONS_VIDEOS:return _F "mp4",
if A in B.EXTENSIONS_ARCHIVES:return _G "m4v",
if A in B.EXTENSIONS_AUDIO:return'Audio' "mov",
if A in B.EXTENSIONS_DOCS:return _H "wmv",
return _B "webm",
async def get_drive_by_extension(A,user_uid,extension):B=await A.get_drive_name_by_extension(extension);return await A.get_or_create(user_uid=user_uid,name=B) "mkv",
async def get_by_user(C,user_uid,name=None): "mpg",
B=name;D={_C:user_uid} "mpeg",
async for A in C.find(**D): "avi",
if not B:yield A "ogv",
elif A[_A]==B:yield A "ogg",
elif not A[_A]and B==_B:A[_A]=_B;await C.save(A);yield A "flv",
async def get_or_create(B,user_uid,name=None,extensions=None): "3gp",
D=user_uid;C=name;E={_C:D} "3g2",
if C:E[_A]=C ]
async for A in B.get_by_user(**E):return A EXTENSIONS_ARCHIVES = [
A=await B.new();A[_C]=D;A[_A]=C;await B.save(A);return A "zip",
async def prepare_default_drives(B): "rar",
C='drive_uid' "7z",
async for A in B.services.drive_item.find(): "tar",
E=A.extension;D=await B.get_drive_by_extension(A[_C],E) "tar.gz",
if not A[C]==D[_D]:A[C]=D[_D];await B.services.drive_item.save(A) "tar.xz",
async def prepare_default_drives_for_user(A,user_uid):B=user_uid;await A.get_or_create(user_uid=B,name=_B);await A.get_or_create(user_uid=B,name='Shared Drive');await A.get_or_create(user_uid=B,name=_E);await A.get_or_create(user_uid=B,name=_F);await A.get_or_create(user_uid=B,name=_G);await A.get_or_create(user_uid=B,name=_H) "tar.bz2",
async def prepare_all(A): "tar.lzma",
await A.prepare_default_drives() "tar.lz",
async for B in A.services.user.find():await A.prepare_default_drives_for_user(B[_D]) ]
EXTENSIONS_AUDIO = [
"mp3",
"wav",
"ogg",
"flac",
"m4a",
"wma",
"aac",
"opus",
"aiff",
"au",
"mid",
"midi",
]
EXTENSIONS_DOCS = [
"pdf",
"doc",
"docx",
"xls",
"xlsx",
"ppt",
"pptx",
"txt",
"md",
"json",
"csv",
"xml",
"html",
"css",
"js",
"py",
"sql",
"rs",
"toml",
"yml",
"yaml",
"ini",
"conf",
"config",
"log",
"csv",
"tsv",
"java",
"cs",
"csproj",
"scss",
"less",
"sass",
"json",
"lock",
"lock.json",
"jsonl",
]
async def get_drive_name_by_extension(self, extension):
if extension.startswith("."):
extension = extension[1:]
if extension in self.EXTENSIONS_PICTURES:
return "Pictures"
if extension in self.EXTENSIONS_VIDEOS:
return "Videos"
if extension in self.EXTENSIONS_ARCHIVES:
return "Archives"
if extension in self.EXTENSIONS_AUDIO:
return "Audio"
if extension in self.EXTENSIONS_DOCS:
return "Documents"
return "My Drive"
async def get_drive_by_extension(self, user_uid, extension):
name = await self.get_drive_name_by_extension(extension)
return await self.get_or_create(user_uid=user_uid, name=name)
async def get_by_user(self, user_uid, name=None):
kwargs = {"user_uid": user_uid}
async for model in self.find(**kwargs):
if not name:
yield model
elif model["name"] == name:
yield model
elif not model["name"] and name == "My Drive":
model["name"] = "My Drive"
await self.save(model)
yield model
async def get_or_create(self, user_uid, name=None, extensions=None):
kwargs = {"user_uid": user_uid}
if name:
kwargs["name"] = name
async for model in self.get_by_user(**kwargs):
return model
model = await self.new()
model["user_uid"] = user_uid
model["name"] = name
await self.save(model)
return model
async def prepare_default_drives(self):
async for drive_item in self.services.drive_item.find():
extension = drive_item.extension
drive = await self.get_drive_by_extension(drive_item["user_uid"], extension)
if not drive_item["drive_uid"] == drive["uid"]:
drive_item["drive_uid"] = drive["uid"]
await self.services.drive_item.save(drive_item)
async def prepare_default_drives_for_user(self, user_uid):
await self.get_or_create(user_uid=user_uid, name="My Drive")
await self.get_or_create(user_uid=user_uid, name="Shared Drive")
await self.get_or_create(user_uid=user_uid, name="Pictures")
await self.get_or_create(user_uid=user_uid, name="Videos")
await self.get_or_create(user_uid=user_uid, name="Archives")
await self.get_or_create(user_uid=user_uid, name="Documents")
async def prepare_all(self):
await self.prepare_default_drives()
async for user in self.services.user.find():
await self.prepare_default_drives_for_user(user["uid"])
+18 -5
View File
@@ -1,7 +1,20 @@
from snek.system.service import BaseService from snek.system.service import BaseService
class DriveItemService(BaseService): class DriveItemService(BaseService):
mapper_name='drive_item'
async def create(B,drive_uid,name,path,type_,size): mapper_name = "drive_item"
A=await B.new();A['drive_uid']=drive_uid;A['name']=name;A['path']=str(path);A['extension']=str(name).split('.')[-1];A['file_type']=type_;A['file_size']=size
if await B.save(A):return A async def create(self, drive_uid, name, path, type_, size):
C=await A.errors;raise Exception(f"Failed to create drive item: {C}.") model = await self.new()
model["drive_uid"] = drive_uid
model["name"] = name
model["path"] = str(path)
model["extension"] = str(name).split(".")[-1]
model["file_type"] = type_
model["file_size"] = size
if await self.save(model):
return model
errors = await model.errors
print("XXXXXXXXXX")
raise Exception(f"Failed to create drive item: {errors}.")
+62 -25
View File
@@ -1,28 +1,65 @@
_E='message'
_D='object_type'
_C='object_uid'
_B=False
_A='user_uid'
from snek.system.model import now from snek.system.model import now
from snek.system.service import BaseService from snek.system.service import BaseService
class NotificationService(BaseService): class NotificationService(BaseService):
mapper_name='notification' mapper_name = "notification"
async def mark_as_read(B,user_uid,channel_message_uid):
A=await B.get(user_uid,object_uid=channel_message_uid) async def mark_as_read(self, user_uid, channel_message_uid):
if not A:return _B model = await self.get(user_uid, object_uid=channel_message_uid)
A['read_at']=now();await B.save(A);return True if not model:
async def get_unread_stats(A,user_uid):await A.query('SELECT object_type, COUNT(*) as count FROM notification WHERE user_uid=:user_uid AND read_at IS NULL GROUP BY object_type',{_A:user_uid}) return False
async def create(B,object_uid,object_type,user_uid,message): model["read_at"] = now()
A=await B.new();A[_C]=object_uid;A[_D]=object_type;A[_A]=user_uid;A[_E]=message await self.save(model)
if await B.save(A):return A return True
raise Exception(f"Failed to create notification: {A.errors}.")
async def create_channel_message(A,channel_message_uid): async def get_unread_stats(self, user_uid):
E=channel_message_uid;D='new_count';F=await A.services.channel_message.get(uid=E);G=await A.services.user.get(uid=F[_A]);A.app.db.begin() await self.query(
async for B in A.services.channel_member.find(channel_uid=F['channel_uid'],is_banned=_B,is_muted=_B,deleted_at=None): "SELECT object_type, COUNT(*) as count FROM notification WHERE user_uid=:user_uid AND read_at IS NULL GROUP BY object_type",
if not B[D]:B[D]=0 {"user_uid": user_uid},
B[D]+=1;H=await A.services.user.get(uid=B[_A]) )
if not H:continue
await A.services.channel_member.save(B);C=await A.new();C[_C]=E;C[_D]='channel_message';C[_A]=B[_A];C[_E]=f"New message from {G["nick"]} in {B["label"]}." async def create(self, object_uid, object_type, user_uid, message):
try:await A.save(C) model = await self.new()
except Exception:raise Exception(f"Failed to create notification: {C.errors}.") model["object_uid"] = object_uid
A.app.db.commit() model["object_type"] = object_type
model["user_uid"] = user_uid
model["message"] = message
if await self.save(model):
return model
raise Exception(f"Failed to create notification: {model.errors}.")
async def create_channel_message(self, channel_message_uid):
channel_message = await self.services.channel_message.get(
uid=channel_message_uid
)
user = await self.services.user.get(uid=channel_message["user_uid"])
self.app.db.begin()
async for channel_member in self.services.channel_member.find(
channel_uid=channel_message["channel_uid"],
is_banned=False,
is_muted=False,
deleted_at=None,
):
if not channel_member["new_count"]:
channel_member["new_count"] = 0
channel_member["new_count"] += 1
usr = await self.services.user.get(uid=channel_member["user_uid"])
if not usr:
continue
await self.services.channel_member.save(channel_member)
model = await self.new()
model["object_uid"] = channel_message_uid
model["object_type"] = "channel_message"
model["user_uid"] = channel_member["user_uid"]
model["message"] = (
f"New message from {user['nick']} in {channel_member['label']}."
)
try:
await self.save(model)
except Exception:
raise Exception(f"Failed to create notification: {model.errors}.")
self.app.db.commit()
+50 -21
View File
@@ -1,23 +1,52 @@
_B='user_uid'
_A=False
from snek.system.service import BaseService from snek.system.service import BaseService
import asyncio,shutil import asyncio
import shutil
class RepositoryService(BaseService): class RepositoryService(BaseService):
mapper_name='repository' mapper_name = "repository"
async def delete(B,user_uid,name):
A=user_uid;C=asyncio.get_event_loop();D=(await B.services.user.get_repository_path(A)).joinpath(name) async def delete(self, user_uid, name):
try:await C.run_in_executor(None,shutil.rmtree,D) loop = asyncio.get_event_loop()
except Exception as E:print(E) repository_path = (await self.services.user.get_repository_path(user_uid)).joinpath(name)
await super().delete(user_uid=A,name=name) try:
async def exists(B,user_uid,name,**A):A[_B]=user_uid;A['name']=name;return await super().exists(**A) await loop.run_in_executor(None, shutil.rmtree, repository_path)
async def init(D,user_uid,name): except Exception as ex:
B='.git';A=await D.services.user.get_repository_path(user_uid) print(ex)
if not A.exists():A.mkdir(parents=True)
A=A.joinpath(name);A=str(A) await super().delete(user_uid=user_uid, name=name)
if not A.endswith(B):A+=B
E=['git','init','--bare',A];C=await asyncio.subprocess.create_subprocess_exec(*E,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.PIPE);F,G=await C.communicate();return C.returncode==0
async def create(A,user_uid,name,is_private=_A): async def exists(self, user_uid, name, **kwargs):
C=name;D=user_uid kwargs["user_uid"] = user_uid
if await A.exists(user_uid=D,name=C):return _A kwargs["name"] = name
if not await A.init(user_uid=D,name=C):return _A return await super().exists(**kwargs)
B=await A.new();B[_B]=D;B['name']=C;B['is_private']=is_private;return await A.save(B)
async def init(self, user_uid, name):
repository_path = await self.services.user.get_repository_path(user_uid)
if not repository_path.exists():
repository_path.mkdir(parents=True)
repository_path = repository_path.joinpath(name)
repository_path = str(repository_path)
if not repository_path.endswith(".git"):
repository_path += ".git"
command = ['git', 'init', '--bare', repository_path]
process = await asyncio.subprocess.create_subprocess_exec(
*command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
return process.returncode == 0
async def create(self, user_uid, name,is_private=False):
if await self.exists(user_uid=user_uid, name=name):
return False
if not await self.init(user_uid=user_uid, name=name):
return False
model = await self.new()
model["user_uid"] = user_uid
model["name"] = name
model["is_private"] = is_private
return await self.save(model)
+102 -33
View File
@@ -1,36 +1,105 @@
_B=False
_A=True
from snek.model.user import UserModel from snek.model.user import UserModel
from snek.system.service import BaseService from snek.system.service import BaseService
from datetime import datetime
import json
import asyncio
import logging
logger = logging.getLogger(__name__)
from snek.system.model import now
class SocketService(BaseService): class SocketService(BaseService):
class Socket:
def __init__(A,ws,user):A.ws=ws;A.is_connected=_A;A.user=user class Socket:
async def send_json(A,data): def __init__(self, ws, user: UserModel):
if not A.is_connected:return _B self.ws = ws
try:await A.ws.send_json(data) self.is_connected = True
except Exception:A.is_connected=_B self.user = user
return A.is_connected
async def close(A):
if not A.is_connected:return _A async def send_json(self, data):
await A.ws.close();A.is_connected=_B;return _A if not self.is_connected:
def __init__(A,app):super().__init__(app);A.sockets=set();A.users={};A.subscriptions={} return False
async def add(A,ws,user_uid): try:
B=user_uid;C=A.Socket(ws,await A.app.services.user.get(uid=B));A.sockets.add(C) await self.ws.send_json(data)
if not A.users.get(B):A.users[B]=set() except Exception:
A.users[B].add(C) self.is_connected = False
async def subscribe(A,ws,channel_uid,user_uid): return self.is_connected
B=channel_uid
if B not in A.subscriptions:A.subscriptions[B]=set() async def close(self):
C=A.Socket(ws,await A.app.services.user.get(uid=user_uid));A.subscriptions[B].add(C) if not self.is_connected:
async def send_to_user(B,user_uid,message): return True
A=0
for C in B.users.get(user_uid,[]): await self.ws.close()
if await C.send_json(message):A+=1 self.is_connected = False
return A
async def broadcast(A,channel_uid,message): return True
try:
async for B in A.services.channel_member.get_user_uids(channel_uid):print(B,flush=_A);await A.send_to_user(B,message) def __init__(self, app):
except Exception as C:print(C,flush=_A) super().__init__(app)
return _A self.sockets = set()
async def delete(A,ws): self.users = {}
for B in[A for A in A.sockets if A.ws==ws]:await B.close();A.sockets.remove(B) self.subscriptions = {}
self.last_update = str(datetime.now())
async def user_availability_service(self):
logger.info("User availability update service started.")
while True:
logger.info("Updating user availability...")
users_updated = []
for s in self.sockets:
if not s.is_connected:
continue
if not s.user in users_updated:
s.user["last_ping"] = now()
await self.app.services.user.save(s.user)
users_updated.append(s.user)
logger.info(f"Updated user availability for {len(users_updated)} online users.")
await asyncio.sleep(60)
async def add(self, ws, user_uid):
s = self.Socket(ws, await self.app.services.user.get(uid=user_uid))
self.sockets.add(s)
s.user["last_ping"] = now()
await self.app.services.user.save(s.user)
logger.info(f"Added socket for user {s.user['username']}")
if not self.users.get(user_uid):
self.users[user_uid] = set()
self.users[user_uid].add(s)
async def subscribe(self, ws, channel_uid, user_uid):
if channel_uid not in self.subscriptions:
self.subscriptions[channel_uid] = set()
s = self.Socket(ws, await self.app.services.user.get(uid=user_uid))
self.subscriptions[channel_uid].add(s)
async def send_to_user(self, user_uid, message):
count = 0
for s in self.users.get(user_uid, []):
if await s.send_json(message):
count += 1
return count
async def broadcast(self, channel_uid, message):
await self._broadcast(channel_uid, message)
async def _broadcast(self, channel_uid, message):
sent = 0
try:
async for user_uid in self.services.channel_member.get_user_uids(
channel_uid
):
sent += await self.send_to_user(user_uid, message)
except Exception as ex:
print(ex, flush=True)
logger.info(f"Broadcasted a message to {sent} users.")
return True
async def delete(self, ws):
for s in [sock for sock in self.sockets if sock.ws == ws]:
await s.close()
logger.info(f"Removed socket for user {s.user['username']}")
self.sockets.remove(s)
+108 -49
View File
@@ -1,53 +1,112 @@
_B='color'
_A=True
import pathlib import pathlib
from snek.system import security from snek.system import security
from snek.system.service import BaseService from snek.system.service import BaseService
class UserService(BaseService): class UserService(BaseService):
mapper_name='user' mapper_name = "user"
async def get_by_username(A,username):return await A.get(username=username)
async def search(C,query,**D): async def get_by_username(self, username):
A=query;A=A.strip().lower() return await self.get(username=username)
if not A:return[]
B=[] async def search(self, query, **kwargs):
async for E in C.find(username={'ilike':'%'+A+'%'},**D):B.append(E) query = query.strip().lower()
return B if not query:
async def validate_login(C,username,password): return []
A=False;B=await C.get(username=username) results = []
if not B:return A async for result in self.find(username={"ilike": "%" + query + "%"}, **kwargs):
if not await security.verify(password,B['password']):return A results.append(result)
return _A return results
async def save(B,user):
A=user async def validate_login(self, username, password):
if not A[_B]:A[_B]=await B.services.util.random_light_hex_color() model = await self.get(username=username)
return await super().save(A) if not model:
async def authenticate(B,username,password): return False
C=password;A=username;print(A,C,flush=_A);D=await B.validate_login(A,C);print(D,flush=_A) if not await security.verify(password, model["password"]):
if not D:return return False
E=await B.get(username=A,deleted_at=None);return E return True
def get_admin_uids(A):return A.mapper.get_admin_uids()
async def get_repository_path(A,user_uid):return pathlib.Path(f"./drive/repositories/{user_uid}") async def save(self, user):
async def get_static_path(B,user_uid): if not user["color"]:
A=pathlib.Path(f"./drive/{user_uid}/snek/static") user["color"] = await self.services.util.random_light_hex_color()
if not A.exists():return return await super().save(user)
return A
async def get_template_path(B,user_uid): def authenticate_sync(self,username,password):
A=pathlib.Path(f"./drive/{user_uid}/snek/templates") user = self.get_by_username_sync(username)
if not A.exists():return
return A if not user:
async def get_home_folder(B,user_uid): return False
A=pathlib.Path(f"./drive/{user_uid}") if not security.verify_sync(password, user["password"]):
if not A.exists(): return False
try:A.mkdir(parents=_A,exist_ok=_A) return True
except:pass
return A async def authenticate(self, username, password):
async def register(B,email,username,password): success = await self.validate_login(username, password)
C=username if not success:
if await B.exists(username=C):raise Exception('User already exists.') return None
A=await B.new();A['nick']=C;A[_B]=await B.services.util.random_light_hex_color();A.email.value=email;A.username.value=C;A.password.value=await security.hash(password)
if await B.save(A): model = await self.get(username=username, deleted_at=None)
if A: return model
D=await B.services.channel.ensure_public_channel(A['uid'])
if not D:raise Exception('Failed to create public channel.') def get_admin_uids(self):
return A return self.mapper.get_admin_uids()
raise Exception(f"Failed to create user: {A.errors}.")
async def get_repository_path(self, user_uid):
return pathlib.Path(f"./drive/repositories/{user_uid}")
async def get_static_path(self, user_uid):
path = pathlib.Path(f"./drive/{user_uid}/snek/static")
if not path.exists():
return None
return path
async def get_template_path(self, user_uid):
path = pathlib.Path(f"./drive/{user_uid}/snek/templates")
if not path.exists():
return None
return path
def get_by_username_sync(self, username):
user = self.mapper.db["user"].find_one(username=username, deleted_at=None)
return dict(user)
def get_home_folder_by_username(self, username):
user = self.get_by_username_sync(username)
folder = pathlib.Path(f"./drive/{user['uid']}")
if not folder.exists():
try:
folder.mkdir(parents=True, exist_ok=True)
except:
pass
return folder
async def get_home_folder(self, user_uid):
folder = pathlib.Path(f"./drive/{user_uid}")
if not folder.exists():
try:
folder.mkdir(parents=True, exist_ok=True)
except:
pass
return folder
async def register(self, email, username, password):
if await self.exists(username=username):
raise Exception("User already exists.")
model = await self.new()
model["nick"] = username
model["color"] = await self.services.util.random_light_hex_color()
model.email.value = email
model.username.value = username
model.password.value = await security.hash(password)
if await self.save(model):
if model:
channel = await self.services.channel.ensure_public_channel(
model["uid"]
)
if not channel:
raise Exception("Failed to create public channel.")
return model
raise Exception(f"Failed to create user: {model.errors}.")
+32 -12
View File
@@ -1,15 +1,35 @@
_A='user_property'
import json import json
from snek.system.service import BaseService from snek.system.service import BaseService
class UserPropertyService(BaseService): class UserPropertyService(BaseService):
mapper_name=_A mapper_name = "user_property"
async def set(C,user_uid,name,value):A='name';B='user_uid';C.mapper.db[_A].upsert({B:user_uid,A:name,'value':json.dumps(value,default=str)},[B,A])
async def get(B,user_uid,name): async def set(self, user_uid, name, value):
try:return json.loads((await super().get(user_uid=user_uid,name=name))['value']) self.mapper.db["user_property"].upsert(
except Exception as A:print(A);return {
async def search(C,query,**D): "user_uid": user_uid,
A=query;A=A.strip().lower() "name": name,
if not A:raise[] "value": json.dumps(value, default=str),
B=[] },
async for E in C.find(name={'ilike':'%'+A+'%'},**D):B.append(E) ["user_uid", "name"],
return B )
async def get(self, user_uid, name):
try:
return json.loads(
(await super().get(user_uid=user_uid, name=name))["value"]
)
except Exception as ex:
print(ex)
return None
async def search(self, query, **kwargs):
query = query.strip().lower()
if not query:
raise []
results = []
async for result in self.find(name={"ilike": "%" + query + "%"}, **kwargs):
results.append(result)
return results
+11 -1
View File
@@ -1,4 +1,14 @@
import random import random
from snek.system.service import BaseService from snek.system.service import BaseService
class UtilService(BaseService): class UtilService(BaseService):
async def random_light_hex_color(D):A=random.randint(128,255);B=random.randint(128,255);C=random.randint(128,255);return f"#{A:02x}{B:02x}{C:02x}"
async def random_light_hex_color(self):
r = random.randint(128, 255)
g = random.randint(128, 255)
b = random.randint(128, 255)
return f"#{r:02x}{g:02x}{b:02x}"
+484 -205
View File
@@ -1,207 +1,486 @@
_O='branches' import os
_N='message' import aiohttp
_M='author'
_L='Invalid JSON data'
_K='origin'
_J='Repository not found'
_I='main'
_H='repository'
_G='branch'
_F='.git'
_E=None
_D='user'
_C='repo_name'
_B='username'
_A='repository_path'
import os,aiohttp
from aiohttp import web from aiohttp import web
import git,shutil,json,tempfile,asyncio,logging,base64,pathlib
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') import shutil
logger=logging.getLogger('git_server') import json
import tempfile
import asyncio
import logging
import base64
import pathlib
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger('git_server')
class GitApplication(web.Application): class GitApplication(web.Application):
def __init__(A,parent=_E):B='/branches/{repo_name}';A.parent=parent;super().__init__(client_max_size=5368709120);A.REPO_DIR='drive/repositories/3177f85e-dbb3-4406-993e-3d3748fea545';A.USERS={'x':'x','bob':'bobpass'};A.add_routes([web.post('/create/{repo_name}',A.create_repository),web.delete('/delete/{repo_name}',A.delete_repository),web.get('/clone/{repo_name}',A.clone_repository),web.post('/push/{repo_name}',A.push_repository),web.post('/pull/{repo_name}',A.pull_repository),web.get('/status/{repo_name}',A.status_repository),web.get('/list',A.list_repositories),web.get(B,A.list_branches),web.post(B,A.create_branch),web.get('/log/{repo_name}',A.commit_log),web.get('/file/{repo_name}/{file_path:.*}',A.file_content),web.get('/{path:.+}/info/refs',A.git_smart_http),web.post('/{path:.+}/git-upload-pack',A.git_smart_http),web.post('/{path:.+}/git-receive-pack',A.git_smart_http),web.get('/{repo_name}.git/info/refs',A.git_smart_http),web.post('/{repo_name}.git/git-upload-pack',A.git_smart_http),web.post('/{repo_name}.git/git-receive-pack',A.git_smart_http)]) def __init__(self, parent=None):
async def check_basic_auth(B,request): #import git
C='Basic ';A=request;D=A.headers.get('Authorization','') #globals()['git'] = git
if not D.startswith(C):return _E,_E self.parent = parent
E=D.split(C)[1];F=base64.b64decode(E).decode();G,H=F.split(':',1);A[_D]=await B.parent.services.user.authenticate(username=G,password=H) super().__init__(client_max_size=1024*1024*1024*5)
if not A[_D]:return _E,_E self.add_routes([
A[_A]=await B.parent.services.user.get_repository_path(A[_D]['uid']);return A[_D][_B],A[_A] web.post('/create/{repo_name}', self.create_repository),
@staticmethod web.delete('/delete/{repo_name}', self.delete_repository),
def require_auth(handler): web.get('/clone/{repo_name}', self.clone_repository),
async def A(self,request,*D,**E): web.post('/push/{repo_name}', self.push_repository),
A=request;B,C=await self.check_basic_auth(A) web.post('/pull/{repo_name}', self.pull_repository),
if not B or not C:return web.Response(status=401,headers={'WWW-Authenticate':'Basic'},text='Authentication required') web.get('/status/{repo_name}', self.status_repository),
A[_B]=B;A[_A]=C;return await handler(self,A,*D,**E) #web.get('/list', self.list_repositories),
return A web.get('/branches/{repo_name}', self.list_branches),
def repo_path(A,repository_path,repo_name):return repository_path.joinpath(repo_name+_F) web.post('/branches/{repo_name}', self.create_branch),
def check_repo_exists(A,repository_path,repo_name): web.get('/log/{repo_name}', self.commit_log),
B=A.repo_path(repository_path,repo_name) web.get('/file/{repo_name}/{file_path:.*}', self.file_content),
if not os.path.exists(B):return web.Response(text=_J,status=404) web.get('/{path:.+}/info/refs', self.git_smart_http),
@require_auth web.post('/{path:.+}/git-upload-pack', self.git_smart_http),
async def create_repository(self,request): web.post('/{path:.+}/git-receive-pack', self.git_smart_http),
B=request;E=B[_B];A=B.match_info[_C];F=B[_A] web.get('/{repo_name}.git/info/refs', self.git_smart_http),
if not A or'/'in A or'..'in A:return web.Response(text='Invalid repository name',status=400) web.post('/{repo_name}.git/git-upload-pack', self.git_smart_http),
C=self.repo_path(F,A) web.post('/{repo_name}.git/git-receive-pack', self.git_smart_http),
if os.path.exists(C):return web.Response(text='Repository already exists',status=400) ])
try:git.Repo.init(C,bare=True);logger.info(f"Created repository: {A} for user {E}");return web.Response(text=f"Created repository {A}")
except Exception as D:logger.error(f"Error creating repository {A}: {str(D)}");return web.Response(text=f"Error creating repository: {str(D)}",status=500)
@require_auth async def check_basic_auth(self, request):
async def delete_repository(self,request): auth_header = request.headers.get("Authorization", "")
B=request;F=B[_B];A=B.match_info[_C];C=B[_A];D=self.check_repo_exists(C,A) if not auth_header.startswith("Basic "):
if D:return D return None,None
try:shutil.rmtree(self.repo_path(C,A));logger.info(f"Deleted repository: {A} for user {F}");return web.Response(text=f"Deleted repository {A}") encoded_creds = auth_header.split("Basic ")[1]
except Exception as E:logger.error(f"Error deleting repository {A}: {str(E)}");return web.Response(text=f"Error deleting repository: {str(E)}",status=500) decoded_creds = base64.b64decode(encoded_creds).decode()
@require_auth username, password = decoded_creds.split(":", 1)
async def clone_repository(self,request): request["user"] = await self.parent.services.user.authenticate(
A=request;H=A[_B];B=A.match_info[_C];E=A[_A];C=self.check_repo_exists(E,B) username=username, password=password
if C:return C )
F=A.host;D=f"http://{F}/{B}.git";G={_H:B,'clone_command':f"git clone {D}",'clone_url':D};return web.json_response(G) if not request["user"]:
@require_auth return None,None
async def push_repository(self,request): request["repository_path"] = await self.parent.services.user.get_repository_path(
B=request;L=B[_B];C=B.match_info[_C];E=B[_A];F=self.check_repo_exists(E,C) request["user"]["uid"]
if F:return F )
try:D=await B.json()
except json.JSONDecodeError:return web.Response(text=_L,status=400) return request["user"]['username'],request["repository_path"]
M=D.get('commit_message','Update from server');G=D.get(_G,_I);H=D.get('changes',[])
if not H:return web.Response(text='No changes provided',status=400)
with tempfile.TemporaryDirectory()as I: @staticmethod
A=git.Repo.clone_from(self.repo_path(E,C),I) def require_auth(handler):
for J in H: async def wrapped(self, request, *args, **kwargs):
K=os.path.join(I,J.get('file',''));N=J.get('content','');os.makedirs(os.path.dirname(K),exist_ok=True) username, repository_path = await self.check_basic_auth(request)
with open(K,'w')as O:O.write(N) if not username or not repository_path:
A.git.add(A=True) return web.Response(status=401, headers={'WWW-Authenticate': 'Basic'}, text='Authentication required')
if not A.config_reader().has_section(_D):A.config_writer().set_value(_D,'name','Git Server').release();A.config_writer().set_value(_D,'email','git@server.local').release() request['username'] = username
A.index.commit(M);P=A.remote(_K);P.push(refspec=f"{G}:{G}") request['repository_path'] = repository_path
logger.info(f"Pushed to repository: {C} for user {L}");return web.Response(text=f"Successfully pushed changes to {C}") return await handler(self, request, *args, **kwargs)
@require_auth return wrapped
async def pull_repository(self,request):
C=request;K=C[_B];A=C.match_info[_C];H=C[_A];I=self.check_repo_exists(H,A) def repo_path(self, repository_path, repo_name):
if I:return I return repository_path.joinpath(repo_name + '.git')
try:E=await C.json()
except json.JSONDecodeError:E={} def check_repo_exists(self, repository_path, repo_name):
B=E.get('remote_url');L=E.get(_G,_I) repo_dir = self.repo_path(repository_path, repo_name)
if not B:return web.Response(text='Remote URL is required',status=400) if not os.path.exists(repo_dir):
with tempfile.TemporaryDirectory()as M: return web.Response(text="Repository not found", status=404)
try: return None
D=git.Repo.clone_from(self.repo_path(H,A),M);F='pull_source'
try:G=D.create_remote(F,B) @require_auth
except git.GitCommandError:G=D.remote(F);G.set_url(B) async def create_repository(self, request):
G.fetch();D.git.merge(f"{F}/{L}");N=D.remote(_K);N.push();logger.info(f"Pulled to repository {A} from {B} for user {K}");return web.Response(text=f"Successfully pulled changes from {B} to {A}") username = request['username']
except Exception as J:logger.error(f"Error pulling to {A}: {str(J)}");return web.Response(text=f"Error pulling changes: {str(J)}",status=500) repo_name = request.match_info['repo_name']
@require_auth repository_path = request['repository_path']
async def status_repository(self,request): if not repo_name or '/' in repo_name or '..' in repo_name:
C=request;S=C[_B];B=C.match_info[_C];F=C[_A];G=self.check_repo_exists(F,B) return web.Response(text="Invalid repository name", status=400)
if G:return G repo_dir = self.repo_path(repository_path, repo_name)
with tempfile.TemporaryDirectory()as D: if os.path.exists(repo_dir):
try: return web.Response(text="Repository already exists", status=400)
E=git.Repo.clone_from(self.repo_path(F,B),D);L=[A.name for A in E.branches];M=E.active_branch.name;H=[] try:
for A in list(E.iter_commits(max_count=5)):H.append({'id':A.hexsha,_M:f"{A.author.name} <{A.author.email}>",'date':A.committed_datetime.isoformat(),_N:A.message}) git.Repo.init(repo_dir, bare=True)
I=[] logger.info(f"Created repository: {repo_name} for user {username}")
for(J,T,N)in os.walk(D): return web.Response(text=f"Created repository {repo_name}")
if _F in J:continue except Exception as e:
for O in N:P=os.path.join(J,O);Q=os.path.relpath(P,D);I.append(Q) logger.error(f"Error creating repository {repo_name}: {str(e)}")
R={_H:B,_O:L,'active_branch':M,'recent_commits':H,'files':I};return web.json_response(R) return web.Response(text=f"Error creating repository: {str(e)}", status=500)
except Exception as K:logger.error(f"Error getting status for {B}: {str(K)}");return web.Response(text=f"Error getting repository status: {str(K)}",status=500)
@require_auth @require_auth
async def list_repositories(self,request): async def delete_repository(self, request):
D=request;G=D[_B] username = request['username']
try: repo_name = request.match_info['repo_name']
A=[];B=self.REPO_DIR repository_path = request['repository_path']
if os.path.exists(B): error_response = self.check_repo_exists(repository_path, repo_name)
for C in os.listdir(B): if error_response:
F=os.path.join(B,C) return error_response
if os.path.isdir(F)and C.endswith(_F):A.append(C[:-4]) #'''
if D.query.get('format')=='json':return web.json_response({'repositories':A}) try:
else:return web.Response(text='\n'.join(A)if A else'No repositories found') shutil.rmtree(self.repo_path(repository_path, repo_name))
except Exception as E:logger.error(f"Error listing repositories: {str(E)}");return web.Response(text=f"Error listing repositories: {str(E)}",status=500) logger.info(f"Deleted repository: {repo_name} for user {username}")
@require_auth return web.Response(text=f"Deleted repository {repo_name}")
async def list_branches(self,request): except Exception as e:
A=request;H=A[_B];B=A.match_info[_C];C=A[_A];D=self.check_repo_exists(C,B) logger.error(f"Error deleting repository {repo_name}: {str(e)}")
if D:return D return web.Response(text=f"Error deleting repository: {str(e)}", status=500)
with tempfile.TemporaryDirectory()as E:F=git.Repo.clone_from(self.repo_path(C,B),E);G=[A.name for A in F.branches];return web.json_response({_O:G})
@require_auth @require_auth
async def create_branch(self,request): async def clone_repository(self, request):
B=request;I=B[_B];C=B.match_info[_C];D=B[_A];E=self.check_repo_exists(D,C) username = request['username']
if E:return E repo_name = request.match_info['repo_name']
try:F=await B.json() repository_path = request['repository_path']
except json.JSONDecodeError:return web.Response(text=_L,status=400) error_response = self.check_repo_exists(repository_path, repo_name)
A=F.get('branch_name');J=F.get('start_point','HEAD') if error_response:
if not A:return web.Response(text='Branch name is required',status=400) return error_response
with tempfile.TemporaryDirectory()as K: host = request.host
try:G=git.Repo.clone_from(self.repo_path(D,C),K);G.git.branch(A,J);G.git.push(_K,A);logger.info(f"Created branch {A} in repository {C} for user {I}");return web.Response(text=f"Created branch {A}") clone_url = f"http://{host}/{repo_name}.git"
except Exception as H:logger.error(f"Error creating branch {A} in {C}: {str(H)}");return web.Response(text=f"Error creating branch: {str(H)}",status=500) response_data = {
@require_auth "repository": repo_name,
async def commit_log(self,request): "clone_command": f"git clone {clone_url}",
B=request;L=B[_B];C=B.match_info[_C];F=B[_A];G=self.check_repo_exists(F,C) "clone_url": clone_url
if G:return G }
try:I=int(B.query.get('limit',10));H=B.query.get(_G,_I) return web.json_response(response_data)
except ValueError:return web.Response(text='Invalid limit parameter',status=400)
with tempfile.TemporaryDirectory()as J: @require_auth
try: async def push_repository(self, request):
K=git.Repo.clone_from(self.repo_path(F,C),J);E=[] username = request['username']
try: repo_name = request.match_info['repo_name']
for A in list(K.iter_commits(H,max_count=I)):E.append({'id':A.hexsha,'short_id':A.hexsha[:7],_M:f"{A.author.name} <{A.author.email}>",'date':A.committed_datetime.isoformat(),_N:A.message.strip()}) repository_path = request['repository_path']
except git.GitCommandError as D: error_response = self.check_repo_exists(repository_path, repo_name)
if'unknown revision or path'in str(D):E=[] if error_response:
else:raise return error_response
return web.json_response({_H:C,_G:H,'commits':E}) try:
except Exception as D:logger.error(f"Error getting commit log for {C}: {str(D)}");return web.Response(text=f"Error getting commit log: {str(D)}",status=500) data = await request.json()
@require_auth except json.JSONDecodeError:
async def file_content(self,request): return web.Response(text="Invalid JSON data", status=400)
A=request;N=A[_B];B=A.match_info[_C];C=A.match_info.get('file_path','');E=A.query.get(_G,_I);F=A[_A];G=self.check_repo_exists(F,B) commit_message = data.get('commit_message', 'Update from server')
if G:return G branch = data.get('branch', 'main')
with tempfile.TemporaryDirectory()as H: changes = data.get('changes', [])
try: if not changes:
J=git.Repo.clone_from(self.repo_path(F,B),H) return web.Response(text="No changes provided", status=400)
try:J.git.checkout(E) with tempfile.TemporaryDirectory() as temp_dir:
except git.GitCommandError:return web.Response(text=f"Branch '{E}' not found",status=404) temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
D=os.path.join(H,C) for change in changes:
if not os.path.exists(D):return web.Response(text=f"File '{C}' not found",status=404) file_path = os.path.join(temp_dir, change.get('file', ''))
if os.path.isdir(D):K=os.listdir(D);return web.json_response({_H:B,'path':C,'type':'directory','contents':K}) content = change.get('content', '')
else: os.makedirs(os.path.dirname(file_path), exist_ok=True)
try: with open(file_path, 'w') as f:
with open(D,'r')as L:M=L.read() f.write(content)
return web.Response(text=M) temp_repo.git.add(A=True)
except UnicodeDecodeError:return web.Response(text=f"Cannot display binary file content for '{C}'",status=400) if not temp_repo.config_reader().has_section('user'):
except Exception as I:logger.error(f"Error getting file content from {B}: {str(I)}");return web.Response(text=f"Error getting file content: {str(I)}",status=500) temp_repo.config_writer().set_value("user", "name", "Git Server").release()
@require_auth temp_repo.config_writer().set_value("user", "email", "git@server.local").release()
async def git_smart_http(self,request): temp_repo.index.commit(commit_message)
B='POST';G='git-receive-pack';H='git-upload-pack';I='Content-Type';J='--stateless-rpc';D='/git-receive-pack';E='/git-upload-pack';F='/info/refs';A=request;P=A[_B];N=A[_A];C=A.path origin = temp_repo.remote('origin')
async def K(): origin.push(refspec=f"{branch}:{branch}")
B=C.lstrip('/') logger.info(f"Pushed to repository: {repo_name} for user {username}")
if B.endswith(F):A=B[:-len(F)] return web.Response(text=f"Successfully pushed changes to {repo_name}")
elif B.endswith(E):A=B[:-len(E)]
elif B.endswith(D):A=B[:-len(D)] @require_auth
else:A=B async def pull_repository(self, request):
if A.endswith(_F):A=A[:-4] username = request['username']
A=A[4:];G=N.joinpath(A+_F);logger.info(f"Resolved repo path: {G}");return G repo_name = request.match_info['repo_name']
async def O(service): repository_path = request['repository_path']
C=service;D=await K();logger.info(f"handle_info_refs: {D}") error_response = self.check_repo_exists(repository_path, repo_name)
if not os.path.exists(D):return web.Response(text=_J,status=404) if error_response:
L=[C,J,'--advertise-refs',str(D)] return error_response
try: try:
E=await asyncio.create_subprocess_exec(*L,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.PIPE);M,F=await E.communicate() data = await request.json()
if E.returncode!=0:logger.error(f"Git command failed: {F.decode()}");return web.Response(text=f"Git error: {F.decode()}",status=500) except json.JSONDecodeError:
B=web.StreamResponse(status=200,reason='OK',headers={I:f"application/x-{C}-advertisement",'Cache-Control':'no-cache'});await B.prepare(A);G=f"# service={C}\n";N=len(G)+4;O=f"{N:04x}";await B.write(f"{O}{G}0000".encode());await B.write(M);return B data = {}
except Exception as H:logger.error(f"Error handling info/refs: {str(H)}");return web.Response(text=f"Server error: {str(H)}",status=500) remote_url = data.get('remote_url')
async def L(service): branch = data.get('branch', 'main')
B=service;C=await K();logger.info(f"handle_service_rpc: {C}") if not remote_url:
if not os.path.exists(C):return web.Response(text=_J,status=404) return web.Response(text="Remote URL is required", status=400)
if not A.headers.get(I)==f"application/x-{B}-request":return web.Response(text='Invalid Content-Type',status=403) with tempfile.TemporaryDirectory() as temp_dir:
G=await A.read();H=[B,J,str(C)] try:
try: local_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
D=await asyncio.create_subprocess_exec(*H,stdin=asyncio.subprocess.PIPE,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.PIPE);L,E=await D.communicate(input=G) remote_name = "pull_source"
if D.returncode!=0:logger.error(f"Git command failed: {E.decode()}");return web.Response(text=f"Git error: {E.decode()}",status=500) try:
return web.Response(body=L,content_type=f"application/x-{B}-result") remote = local_repo.create_remote(remote_name, remote_url)
except Exception as F:logger.error(f"Error handling service RPC: {str(F)}");return web.Response(text=f"Server error: {str(F)}",status=500) except git.GitCommandError:
if A.method=='GET'and C.endswith(F): remote = local_repo.remote(remote_name)
M=A.query.get('service') remote.set_url(remote_url)
if M in(H,G):return await O(M) remote.fetch()
else:return web.Response(text='Smart HTTP requires service parameter',status=400) local_repo.git.merge(f"{remote_name}/{branch}")
elif A.method==B and E in C:return await L(H) origin = local_repo.remote('origin')
elif A.method==B and D in C:return await L(G) origin.push()
return web.Response(text='Not found',status=404) logger.info(f"Pulled to repository {repo_name} from {remote_url} for user {username}")
if __name__=='__main__': return web.Response(text=f"Successfully pulled changes from {remote_url} to {repo_name}")
try:import uvloop;asyncio.set_event_loop_policy(uvloop.EventLoopPolicy());logger.info('Using uvloop for improved performance') except Exception as e:
except ImportError:logger.info('uvloop not available, using standard event loop') logger.error(f"Error pulling to {repo_name}: {str(e)}")
app=GitApplication();logger.info('Starting Git server on port 8080');web.run_app(app,port=8080) return web.Response(text=f"Error pulling changes: {str(e)}", status=500)
@require_auth
async def status_repository(self, request):
username = request['username']
repo_name = request.match_info['repo_name']
repository_path = request['repository_path']
error_response = self.check_repo_exists(repository_path, repo_name)
if error_response:
return error_response
with tempfile.TemporaryDirectory() as temp_dir:
try:
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
branches = [b.name for b in temp_repo.branches]
active_branch = temp_repo.active_branch.name
commits = []
for commit in list(temp_repo.iter_commits(max_count=5)):
commits.append({
"id": commit.hexsha,
"author": f"{commit.author.name} <{commit.author.email}>",
"date": commit.committed_datetime.isoformat(),
"message": commit.message
})
files = []
for root, dirs, filenames in os.walk(temp_dir):
if '.git' in root:
continue
for filename in filenames:
full_path = os.path.join(root, filename)
rel_path = os.path.relpath(full_path, temp_dir)
files.append(rel_path)
status_info = {
"repository": repo_name,
"branches": branches,
"active_branch": active_branch,
"recent_commits": commits,
"files": files
}
return web.json_response(status_info)
except Exception as e:
logger.error(f"Error getting status for {repo_name}: {str(e)}")
return web.Response(text=f"Error getting repository status: {str(e)}", status=500)
@require_auth
async def list_repositories(self, request):
username = request['username']
try:
repos = []
user_dir = self.REPO_DIR
if os.path.exists(user_dir):
for item in os.listdir(user_dir):
item_path = os.path.join(user_dir, item)
if os.path.isdir(item_path) and item.endswith('.git'):
repos.append(item[:-4])
if request.query.get('format') == 'json':
return web.json_response({"repositories": repos})
else:
return web.Response(text="\n".join(repos) if repos else "No repositories found")
except Exception as e:
logger.error(f"Error listing repositories: {str(e)}")
return web.Response(text=f"Error listing repositories: {str(e)}", status=500)
@require_auth
async def list_branches(self, request):
username = request['username']
repo_name = request.match_info['repo_name']
repository_path = request['repository_path']
error_response = self.check_repo_exists(repository_path, repo_name)
if error_response:
return error_response
with tempfile.TemporaryDirectory() as temp_dir:
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
branches = [b.name for b in temp_repo.branches]
return web.json_response({"branches": branches})
@require_auth
async def create_branch(self, request):
username = request['username']
repo_name = request.match_info['repo_name']
repository_path = request['repository_path']
error_response = self.check_repo_exists(repository_path, repo_name)
if error_response:
return error_response
try:
data = await request.json()
except json.JSONDecodeError:
return web.Response(text="Invalid JSON data", status=400)
branch_name = data.get('branch_name')
start_point = data.get('start_point', 'HEAD')
if not branch_name:
return web.Response(text="Branch name is required", status=400)
with tempfile.TemporaryDirectory() as temp_dir:
try:
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
temp_repo.git.branch(branch_name, start_point)
temp_repo.git.push('origin', branch_name)
logger.info(f"Created branch {branch_name} in repository {repo_name} for user {username}")
return web.Response(text=f"Created branch {branch_name}")
except Exception as e:
logger.error(f"Error creating branch {branch_name} in {repo_name}: {str(e)}")
return web.Response(text=f"Error creating branch: {str(e)}", status=500)
@require_auth
async def commit_log(self, request):
username = request['username']
repo_name = request.match_info['repo_name']
repository_path = request['repository_path']
error_response = self.check_repo_exists(repository_path, repo_name)
if error_response:
return error_response
try:
limit = int(request.query.get('limit', 10))
branch = request.query.get('branch', 'main')
except ValueError:
return web.Response(text="Invalid limit parameter", status=400)
with tempfile.TemporaryDirectory() as temp_dir:
try:
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
commits = []
try:
for commit in list(temp_repo.iter_commits(branch, max_count=limit)):
commits.append({
"id": commit.hexsha,
"short_id": commit.hexsha[:7],
"author": f"{commit.author.name} <{commit.author.email}>",
"date": commit.committed_datetime.isoformat(),
"message": commit.message.strip()
})
except git.GitCommandError as e:
if "unknown revision or path" in str(e):
commits = []
else:
raise
return web.json_response({
"repository": repo_name,
"branch": branch,
"commits": commits
})
except Exception as e:
logger.error(f"Error getting commit log for {repo_name}: {str(e)}")
return web.Response(text=f"Error getting commit log: {str(e)}", status=500)
@require_auth
async def file_content(self, request):
username = request['username']
repo_name = request.match_info['repo_name']
file_path = request.match_info.get('file_path', '')
branch = request.query.get('branch', 'main')
repository_path = request['repository_path']
error_response = self.check_repo_exists(repository_path, repo_name)
if error_response:
return error_response
with tempfile.TemporaryDirectory() as temp_dir:
try:
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
try:
temp_repo.git.checkout(branch)
except git.GitCommandError:
return web.Response(text=f"Branch '{branch}' not found", status=404)
file_full_path = os.path.join(temp_dir, file_path)
if not os.path.exists(file_full_path):
return web.Response(text=f"File '{file_path}' not found", status=404)
if os.path.isdir(file_full_path):
files = os.listdir(file_full_path)
return web.json_response({
"repository": repo_name,
"path": file_path,
"type": "directory",
"contents": files
})
else:
try:
with open(file_full_path, 'r') as f:
content = f.read()
return web.Response(text=content)
except UnicodeDecodeError:
return web.Response(text=f"Cannot display binary file content for '{file_path}'", status=400)
except Exception as e:
logger.error(f"Error getting file content from {repo_name}: {str(e)}")
return web.Response(text=f"Error getting file content: {str(e)}", status=500)
@require_auth
async def git_smart_http(self, request):
username = request['username']
repository_path = request['repository_path']
path = request.path
async def get_repository_path():
req_path = path.lstrip('/')
if req_path.endswith('/info/refs'):
repo_name = req_path[:-len('/info/refs')]
elif req_path.endswith('/git-upload-pack'):
repo_name = req_path[:-len('/git-upload-pack')]
elif req_path.endswith('/git-receive-pack'):
repo_name = req_path[:-len('/git-receive-pack')]
else:
repo_name = req_path
if repo_name.endswith('.git'):
repo_name = repo_name[:-4]
repo_name = repo_name[4:]
repo_dir = repository_path.joinpath(repo_name + ".git")
logger.info(f"Resolved repo path: {repo_dir}")
return repo_dir
async def handle_info_refs(service):
repo_path = await get_repository_path()
logger.info(f"handle_info_refs: {repo_path}")
if not os.path.exists(repo_path):
return web.Response(text="Repository not found", status=404)
cmd = [service, '--stateless-rpc', '--advertise-refs', str(repo_path)]
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
logger.error(f"Git command failed: {stderr.decode()}")
return web.Response(text=f"Git error: {stderr.decode()}", status=500)
response = web.StreamResponse(
status=200,
reason='OK',
headers={
'Content-Type': f'application/x-{service}-advertisement',
'Cache-Control': 'no-cache'
}
)
await response.prepare(request)
packet = f"# service={service}\n"
length = len(packet) + 4
header = f"{length:04x}"
await response.write(f"{header}{packet}0000".encode())
await response.write(stdout)
return response
except Exception as e:
logger.error(f"Error handling info/refs: {str(e)}")
return web.Response(text=f"Server error: {str(e)}", status=500)
async def handle_service_rpc(service):
repo_path = await get_repository_path()
logger.info(f"handle_service_rpc: {repo_path}")
if not os.path.exists(repo_path):
return web.Response(text="Repository not found", status=404)
if not request.headers.get('Content-Type') == f'application/x-{service}-request':
return web.Response(text="Invalid Content-Type", status=403)
body = await request.read()
cmd = [service, '--stateless-rpc', str(repo_path)]
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate(input=body)
if process.returncode != 0:
logger.error(f"Git command failed: {stderr.decode()}")
return web.Response(text=f"Git error: {stderr.decode()}", status=500)
return web.Response(
body=stdout,
content_type=f'application/x-{service}-result'
)
except Exception as e:
logger.error(f"Error handling service RPC: {str(e)}")
return web.Response(text=f"Server error: {str(e)}", status=500)
if request.method == 'GET' and path.endswith('/info/refs'):
service = request.query.get('service')
if service in ('git-upload-pack', 'git-receive-pack'):
return await handle_info_refs(service)
else:
return web.Response(text="Smart HTTP requires service parameter", status=400)
elif request.method == 'POST' and '/git-upload-pack' in path:
return await handle_service_rpc('git-upload-pack')
elif request.method == 'POST' and '/git-receive-pack' in path:
return await handle_service_rpc('git-receive-pack')
return web.Response(text="Not found", status=404)
if __name__ == '__main__':
try:
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
logger.info("Using uvloop for improved performance")
except ImportError:
logger.info("uvloop not available, using standard event loop")
app = GitApplication()
logger.info("Starting Git server on port 8080")
web.run_app(app, port=8080)
+116
View File
@@ -0,0 +1,116 @@
import aiohttp
ENABLED = False
import aiohttp
import asyncio
from aiohttp import web
import sqlite3
import dataset
from sqlalchemy import event
from sqlalchemy.engine import Engine
import json
queue = asyncio.Queue()
class State:
do_not_sync = False
async def sync_service(app):
if not ENABLED:
return
session = aiohttp.ClientSession()
async with session.ws_connect('http://localhost:3131/ws') as ws:
async def receive():
queries_synced = 0
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
try:
data = json.loads(msg.data)
State.do_not_sync = True
app.db.execute(*data)
app.db.commit()
State.do_not_sync = False
queries_synced += 1
print("queries synced: " + str(queries_synced))
print(*data)
await app.services.socket.broadcast_event()
except Exception as e:
print(e)
pass
#print(f"Received: {msg.data}")
elif msg.type == aiohttp.WSMsgType.ERROR:
break
async def write():
while True:
msg = await queue.get()
await ws.send_str(json.dumps(msg,default=str))
queue.task_done()
await asyncio.gather(receive(), write())
await session.close()
queries_queued = 0
# Attach a listener to log all executed statements
@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
if not ENABLED:
return
global queries_queued
if State.do_not_sync:
print(statement,parameters)
return
if statement.startswith("SELECT"):
return
queue.put_nowait((statement, parameters))
queries_queued += 1
print("Queries queued: " + str(queries_queued))
async def websocket_handler(request):
queries_broadcasted = 0
ws = web.WebSocketResponse()
await ws.prepare(request)
request.app['websockets'].append(ws)
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
for client in request.app['websockets']:
if client != ws:
await client.send_str(msg.data)
cursor = request.app['db'].cursor()
data = json.loads(msg.data)
queries_broadcasted += 1
cursor.execute(*data)
cursor.close()
print("Queries broadcasted: " + str(queries_broadcasted))
elif msg.type == aiohttp.WSMsgType.ERROR:
print(f'WebSocket connection closed with exception {ws.exception()}')
request.app['websockets'].remove(ws)
return ws
app = web.Application()
app['websockets'] = []
app.router.add_get('/ws', websocket_handler)
async def on_startup(app):
app['db'] = sqlite3.connect('snek.db')
print("Server starting...")
async def on_cleanup(app):
for ws in app['websockets']:
await ws.close()
app['db'].close()
app.on_startup.append(on_startup)
app.on_cleanup.append(on_cleanup)
if __name__ == '__main__':
web.run_app(app, host='127.0.0.1', port=3131)
+76
View File
@@ -0,0 +1,76 @@
import asyncssh
import logging
from pathlib import Path
global _app
def set_app(app):
global _app
_app = app
def get_app():
return _app
logger = logging.getLogger(__name__)
roots = {}
class SFTPServer(asyncssh.SFTPServer):
def __init__(self, chan: asyncssh.SSHServerChannel):
self.root = get_app().services.user.get_home_folder_by_username(
chan.get_extra_info('username')
)
self.root.mkdir(exist_ok=True)
self.root = str(self.root)
super().__init__(chan, chroot=self.root)
def map_path(self, path):
mapped_path = Path(self.root).joinpath(path.lstrip(b"/").decode())
logger.debug(f"Mapping client path {path} to {mapped_path}")
return str(mapped_path).encode()
class SSHServer(asyncssh.SSHServer):
def password_auth_supported(self):
return True
def validate_password(self, username, password):
logger.debug(f"Validating credentials for user {username}")
result = get_app().services.user.authenticate_sync(username,password)
logger.info(f"Validating credentials for user {username}: {result}")
return result
async def start_ssh_server(app,host,port):
set_app(app)
logger.info("Starting SFTP server setup")
host_key_path = Path("drive") / ".ssh" / "sftp_server_key"
host_key_path.parent.mkdir(exist_ok=True, parents=True)
try:
if not host_key_path.exists():
logger.info(f"Generating new host key at {host_key_path}")
key = asyncssh.generate_private_key("ecdsa-sha2-nistp256")
key.write_private_key(host_key_path)
else:
logger.info(f"Loading existing host key from {host_key_path}")
key = asyncssh.read_private_key(host_key_path)
except Exception as e:
logger.error(f"Failed to generate or load host key: {e}")
raise
logger.info(f"Starting SFTP server on 127.0.0.1:{port}")
try:
x = await asyncssh.listen(
host=host,
port=port,
#process_factory=handle_client,
server_host_keys=[key],
server_factory=SSHServer,
sftp_factory=SFTPServer
)
return x
except Exception as e:
logger.warning(f"Failed to start SFTP server. Already running.")
pass
+223 -191
View File
@@ -1,235 +1,267 @@
// Written by retoor@molodetz.nl // Written by retoor@molodetz.nl
// This project implements a client-server communication system using WebSockets and REST APIs. // 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. // 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. // No additional imports were used beyond standard JavaScript objects and constructors.
// MIT License // MIT License
import { Schedule } from './schedule.js'; import { Schedule } from "./schedule.js";
import { EventHandler } from "./event-handler.js"; import { EventHandler } from "./event-handler.js";
import { Socket } from "./socket.js"; import { Socket } from "./socket.js";
export class RESTClient { export class RESTClient {
debug = false; debug = false;
async get(url, params = {}) { async get(url, params = {}) {
const encodedParams = new URLSearchParams(params); const encodedParams = new URLSearchParams(params);
if (encodedParams) url += '?' + encodedParams; if (encodedParams) url += "?" + encodedParams;
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
}); });
const result = await response.json(); const result = await response.json();
if (this.debug) { if (this.debug) {
console.debug({ url, params, result }); console.debug({ url, params, result });
}
return result;
} }
return result;
}
async post(url, data) { async post(url, data) {
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
const result = await response.json(); const result = await response.json();
if (this.debug) { if (this.debug) {
console.debug({ url, data, result }); console.debug({ url, data, result });
}
return result;
} }
return result;
}
} }
export class Chat extends EventHandler { export class Chat extends EventHandler {
constructor() { constructor() {
super(); super();
this._url = window.location.hostname === 'localhost' ? 'ws://localhost/chat.ws' : 'wss://' + window.location.hostname + '/chat.ws'; this._url =
this._socket = null; window.location.hostname === "localhost"
this._waitConnect = null; ? "ws://localhost/chat.ws"
this._promises = {}; : "wss://" + window.location.hostname + "/chat.ws";
this._socket = null;
this._waitConnect = null;
this._promises = {};
}
connect() {
if (this._waitConnect) {
return this._waitConnect;
} }
return new Promise((resolve) => {
this._waitConnect = resolve;
console.debug("Connecting..");
try {
this._socket = new WebSocket(this._url);
} catch (e) {
console.warn(e);
setTimeout(() => {
this.ensureConnection();
}, 1000);
}
connect() { this._socket.onconnect = () => {
if (this._waitConnect) { this._connected();
return this._waitConnect; this._waitSocket();
} };
return new Promise((resolve) => { });
this._waitConnect = resolve; }
console.debug("Connecting..");
try { generateUniqueId() {
this._socket = new WebSocket(this._url); return "id-" + Math.random().toString(36).substr(2, 9);
} catch (e) { }
console.warn(e);
setTimeout(() => {
this.ensureConnection();
}, 1000);
}
this._socket.onconnect = () => { call(method, ...args) {
this._connected(); return new Promise((resolve, reject) => {
this._waitSocket(); try {
}; const command = { method, args, message_id: this.generateUniqueId() };
}); this._promises[command.message_id] = resolve;
} this._socket.send(JSON.stringify(command));
} catch (e) {
reject(e);
}
});
}
generateUniqueId() { _connected() {
return 'id-' + Math.random().toString(36).substr(2, 9); this._socket.onmessage = (event) => {
} const message = JSON.parse(event.data);
if (message.message_id && this._promises[message.message_id]) {
this._promises[message.message_id](message);
delete this._promises[message.message_id];
} else {
this.emit("message", message);
}
};
this._socket.onclose = () => {
this._waitSocket = null;
this._socket = null;
this.emit("close");
};
}
call(method, ...args) { async privmsg(room, text) {
return new Promise((resolve, reject) => { await rest.post("/api/privmsg", {
try { room,
const command = { method, args, message_id: this.generateUniqueId() }; text,
this._promises[command.message_id] = resolve; });
this._socket.send(JSON.stringify(command)); }
} catch (e) {
reject(e);
}
});
}
_connected() {
this._socket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.message_id && this._promises[message.message_id]) {
this._promises[message.message_id](message);
delete this._promises[message.message_id];
} else {
this.emit("message", message);
}
};
this._socket.onclose = () => {
this._waitSocket = null;
this._socket = null;
this.emit('close');
};
}
async privmsg(room, text) {
await rest.post("/api/privmsg", {
room,
text,
});
}
} }
export class NotificationAudio { export class NotificationAudio {
constructor(timeout = 500) { constructor(timeout = 500) {
this.schedule = new Schedule(timeout); this.schedule = new Schedule(timeout);
} }
sounds = { sounds = {
"message": "/audio/soundfx.d_beep3.mp3", message: "/audio/soundfx.d_beep3.mp3",
"mention": "/audio/750607__deadrobotmusic__notification-sound-1.wav", mention: "/audio/750607__deadrobotmusic__notification-sound-1.wav",
"messageOtherChannel": "/audio/750608__deadrobotmusic__notification-sound-2.wav", messageOtherChannel:
"ping": "/audio/750609__deadrobotmusic__notification-sound-3.wav", "/audio/750608__deadrobotmusic__notification-sound-2.wav",
} ping: "/audio/750609__deadrobotmusic__notification-sound-3.wav",
};
play(soundIndex = 0) { play(soundIndex = 0) {
this.schedule.delay(() => { this.schedule.delay(() => {
new Audio(this.sounds[soundIndex]).play() new Audio(this.sounds[soundIndex])
.then(() => { .play()
console.debug("Gave sound notification"); .then(() => {
}) console.debug("Gave sound notification");
.catch(error => { })
console.error("Notification failed:", error); .catch((error) => {
}); console.error("Notification failed:", error);
}); });
} });
}
} }
export class App extends EventHandler { export class App extends EventHandler {
rest = new RESTClient(); rest = new RESTClient();
ws = null; ws = null;
rpc = null; rpc = null;
audio = null; audio = null;
user = {}; user = {};
typeLock = null;
typeListener = null;
typeEventChannelUid = null;
_debug = false
async set_typing(channel_uid) {
this.typeEventChannel_uid = channel_uid;
}
debug() {
this._debug = !this._debug;
this.ws._debug = this._debug;
}
async ping(...args) { async ping(...args) {
if (this.is_pinging) return false if (this.is_pinging) return false;
this.is_pinging = true this.is_pinging = true;
await this.rpc.ping(...args); await this.rpc.ping(...args);
this.is_pinging = false this.is_pinging = false;
} }
async forcePing(...arg) { async forcePing(...arg) {
await this.rpc.ping(...args); await this.rpc.ping(...args);
} }
starField = null
constructor() {
super();
this.ws = new Socket();
this.rpc = this.ws.client;
this.audio = new NotificationAudio(500);
this.is_pinging = false;
this.ping_interval = setInterval(() => {
this.ping("active");
}, 15000);
this.typeEventChannelUid = null;
this.typeListener = setInterval(() => {
if (this.typeEventChannelUid) {
this.rpc.set_typing(this.typeEventChannelUid);
this.typeEventChannelUid = null;
}
});
constructor() { const me = this;
super(); this.ws.addEventListener("connected", (data) => {
this.ws = new Socket(); this.ping("online");
this.rpc = this.ws.client; });
this.audio = new NotificationAudio(500); this.ws.addEventListener("reconnecting", (data) => {
this.is_pinging = false this.starField?.showNotify("Connecting..","#CC0000")
this.ping_interval = setInterval(() => { })
this.ping("active") this.ws.addEventListener("channel-message", (data) => {
}, 15000) me.emit("channel-message", data);
});
this.ws.addEventListener("data", (data) => {
const me = this if(this._debug){
this.ws.addEventListener("connected", (data) => { console.debug(data)
this.ping("online")
})
this.ws.addEventListener("channel-message", (data) => {
me.emit("channel-message", data);
});
this.rpc.getUser(null).then(user => {
me.user = user;
});
}
playSound(index) {
this.audio.play(index);
}
timeDescription(isoDate) {
const date = new Date(isoDate);
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
return timeStr;
}
timeAgo(date1, date2) {
const diffMs = Math.abs(date2 - date1);
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
if (days) {
return `${days} ${days > 1 ? 'days' : 'day'} ago`;
} }
if (hours) { });
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`; this.rpc.getUser(null).then((user) => {
} me.user = user;
if (minutes) { });
return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`; }
}
return 'just now';
}
async benchMark(times = 100, message = "Benchmark Message") { playSound(index) {
const promises = []; this.audio.play(index);
const me = this; }
for (let i = 0; i < times; i++) {
promises.push(this.rpc.getChannels().then(channels => { timeDescription(isoDate) {
channels.forEach(channel => { const date = new Date(isoDate);
me.rpc.sendMessage(channel.uid, `${message} ${i}`); const hours = String(date.getHours()).padStart(2, "0");
}); const minutes = String(date.getMinutes()).padStart(2, "0");
})); let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
} return timeStr;
}
timeAgo(date1, date2) {
const diffMs = Math.abs(date2 - date1);
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const hours = Math.floor(
(diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60),
);
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
if (days) {
return `${days} ${days > 1 ? "days" : "day"} ago`;
} }
if (hours) {
return `${hours} ${hours > 1 ? "hours" : "hour"} ago`;
}
if (minutes) {
return `${minutes} ${minutes > 1 ? "minutes" : "minute"} ago`;
}
return "just now";
}
async benchMark(times = 100, message = "Benchmark Message") {
const promises = [];
const me = this;
for (let i = 0; i < times; i++) {
promises.push(
this.rpc.getChannels().then((channels) => {
channels.forEach((channel) => {
me.rpc.sendMessage(channel.uid, `${message} ${i}`);
});
}),
);
}
}
} }
export const app = new App(); export const app = new App();
+245 -69
View File
@@ -5,6 +5,10 @@
box-sizing: border-box; box-sizing: border-box;
} }
html {
height: 100%;
}
.gallery { .gallery {
padding: 50px; padding: 50px;
height: auto; height: auto;
@@ -25,33 +29,35 @@ body {
background-color: #000000; background-color: #000000;
color: #e6e6e6; color: #e6e6e6;
line-height: 1.5; line-height: 1.5;
display: flex; display: grid;
flex-direction: column; grid-template-columns: auto 1fr;
height: 100vh; grid-template-rows: auto 1fr;
grid-template-areas:
"header header"
"sidebar chat-area";
min-width: 100%; min-width: 100%;
height: 100%;
} }
main { main {
display: flex; display: flex;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
grid-area: chat-area;
} }
header { header {
background-color: #000000; grid-area: header;
padding-top: 10px; padding: 10px 20px;
padding-left: 20px;
padding-right: 20px;
padding-bottom: 10px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
header .logo { header .logo {
color: #fff; color: #fff;
font-weight: bold; font-weight: bold;
font-size: 1.2em; font-size: 1.2em;
color: #fff; color: #fff;
} }
@@ -81,13 +87,13 @@ a {
} }
h1 { h1 {
font-size: 2em; font-size: 2em;
color: #f05a28; color: #f05a28;
} }
h2 { h2 {
font-size: 1.4em; font-size: 1.4em;
color: #f05a28; color: #f05a28;
} }
@@ -95,13 +101,11 @@ h2 {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #000000;
overflow: hidden; overflow: hidden;
} }
.chat-header { .chat-header {
padding: 10px 20px; padding: 10px 20px;
background-color: #000000;
user-select: none; user-select: none;
} }
@@ -112,10 +116,10 @@ h2 {
} }
footer { footer {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 10px 20px; padding: 10px 20px;
} }
.message-list { .message-list {
@@ -132,7 +136,11 @@ footer {
-ms-overflow-style: none; -ms-overflow-style: none;
padding: 10px; padding: 10px;
height: 10px; height: 10px;
background: #000000; }
.chat-messages {
display: flex;
flex-direction: column;
} }
.container { .container {
@@ -152,6 +160,10 @@ footer {
} }
.chat-messages picture img {
cursor: pointer;
}
.chat-messages::-webkit-scrollbar { .chat-messages::-webkit-scrollbar {
display: none; display: none;
} }
@@ -228,15 +240,14 @@ footer {
.chat-input { .chat-input {
padding: 15px; padding: 15px;
background-color: #000000;
display: flex; display: flex;
align-items: center; align-items: center;
} }
input[type="text"], .chat-input textarea { input[type="text"], .chat-input textarea {
flex: 1; flex: 1;
background-color: #000000;
color: white; color: white;
background: none;
border: none; border: none;
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
@@ -273,13 +284,18 @@ input[type="text"], .chat-input textarea {
} }
.avatar { .avatar {
opacity: 0; opacity: 0;
max-height: 0;
overflow: hidden;
} }
.author, .time { .author {
display: none; display: none;
} }
&:not(:hover, :focus-within, :active) .time {
opacity: 0;
}
} }
.message.switch-user { .message.switch-user {
@@ -291,6 +307,7 @@ input[type="text"], .chat-input textarea {
.avatar { .avatar {
user-select: none; user-select: none;
opacity: 1; opacity: 1;
max-height: unset;
} }
.author { .author {
@@ -298,9 +315,10 @@ input[type="text"], .chat-input textarea {
} }
} }
.message:has(+ .message.switch-user), .message:last-child { .message:has(+ .message.switch-user), .message:has(+ .message.long-time), .message:not(:has(+ .message)) {
.time { .time {
display: block; display: block;
opacity: 1;
} }
} }
@@ -334,11 +352,11 @@ a {
.sidebar { .sidebar {
width: 250px; width: 250px;
background-color: #000000;
padding-left: 20px; padding-left: 20px;
padding-right: 20px; padding-right: 20px;
padding-top: 10px; padding-top: 10px;
overflow-y: auto; overflow-y: auto;
grid-area: sidebar;
} }
.sidebar h2 { .sidebar h2 {
@@ -366,46 +384,204 @@ a {
color: #fff; color: #fff;
} }
@media only screen and (max-width: 768px) { @keyframes glow {
0% {
header{ box-shadow: 0 0 5px #3498db;
top: 0; }
left: 0; 50% {
text-overflow: ellipsis; box-shadow: 0 0 20px #3498db, 0 0 30px #3498db;
width:100%; }
display: flex; 100% {
flex-direction: column; box-shadow: 0 0 5px #3498db;
.logo { }
display:block;
flex: 1;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
h2 {
font-size: 14px;
}
text-align: center;
}
nav {
text-align: right;
flex: 1;
display: block;
width: 100%;
}
}
/*
body {
justify-content: flex-start;
}
header{
position: sticky;
display: block;
.logo {
display:block;
}
}
.chat-input {
position:sticky;
}*/
} }
.glow {
animation: glow 1s;
}
@media only screen and (max-width: 768px) {
header {
top: 0;
left: 0;
text-overflow: ellipsis;
width: 100%;
display: flex;
flex-direction: column;
.logo {
display: block;
flex: 1;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
h2 {
font-size: 14px;
}
text-align: center;
}
nav {
text-align: right;
flex: 1;
display: block;
width: 100%;
}
}
/*
body {
justify-content: flex-start;
}
header{
position: sticky;
display: block;
.logo {
display:block;
}
}
.chat-input {
position:sticky;
}*/
}
dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: none;
border-radius: 12px;
padding: 24px;
background-color: #000; /* Deep black */
color: #f1f1f1;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.8);
width: 90%;
max-width: 400px;
animation: dialogFadeIn 0.3s ease-out, dialogScaleIn 0.3s ease-out;
z-index: 1000;
}
/* Backdrop styling */
dialog::backdrop {
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
}
/* Title and content */
dialog .dialog-title {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 16px;
color: #fff;
}
dialog .dialog-content {
font-size: 1rem;
color: #ccc;
margin-bottom: 20px;
}
/* Button layout */
dialog .dialog-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
dialog .dialog-button {
padding: 8px 16px;
font-size: 0.95rem;
border-radius: 8px;
border: none;
cursor: pointer;
transition: background 0.2s ease;
}
@keyframes dialogFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes dialogScaleIn {
from {
transform: scale(0.95) translate(-50%, -50%);
opacity: 0;
}
to {
transform: scale(1) translate(-50%, -50%);
opacity: 1;
}
}
dialog .dialog-button.primary {
background-color: #f05a28;
color: white;
}
dialog .dialog-button.primary:hover {
background-color: #f05a28;
}
dialog .dialog-button.secondary {
background-color: #f0a328;
color: #eee;
}
dialog .dialog-button.secondary:hover {
background-color: #f0b84c;
}
.embed-url-link {
display: flex;
flex-direction: column;
}
.embed-url-link img,
.embed-url-link video,
.embed-url-link iframe,
.embed-url-link div {
width: auto;
height: auto;
max-width: 100%;
max-height: 400px;
object-fit: contain;
border-radius: 12px 12px 0 0;
}
.embed-url-link .page-site {
font-size: 0.9em;
color: #aaa;
margin-bottom: 5px;
}
.embed-url-link .page-name {
font-size: 1.2em;
color: #f05a28;
margin-bottom: 5px;
}
.embed-url-link .page-description {
font-size: 1em;
color: #e6e6e6;
margin-bottom: 10px;
}
.embed-url-link .page-link {
font-size: 0.9em;
color: #f05a28;
text-decoration: none;
margin-top: 10px;
}
+355 -51
View File
@@ -1,69 +1,373 @@
// Written by retoor@molodetz.nl import { app } from "../app.js";
// This JavaScript class defines a custom HTML element for a chat input widget, featuring a text area and an upload button. It handles user input and triggers events for input changes and message submission. class ChatInputComponent extends HTMLElement {
autoCompletions = {
// Includes standard DOM manipulation methods; no external imports used. "example 1": () => {},
"example 2": () => {},
// MIT License: This code is open-source and can be reused and distributed under the terms of the MIT License. }
hiddenCompletions = {
class ChatInputElement extends HTMLElement { "/starsRender": () => {
_chatWindow = null app.rpc.starsRender(this.channelUid,this.value.replace("/starsRender ",""))
}
}
users = []
textarea = null
_value = ""
lastUpdateEvent = null
previousValue = ""
lastChange = null
changed = false
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.lastUpdateEvent = new Date();
this.component = document.createElement('div'); this.textarea = document.createElement("textarea");
this.shadowRoot.appendChild(this.component); this.value = this.getAttribute("value") || "";
this.previousValue = this.value;
this.lastChange = new Date();
this.changed = false;
} }
set chatWindow(value){
this._chatWindow = value
get value() {
return this._value;
} }
get chatWindow(){
return this._chatWindow
}
get channelUid() {
return this.chatWindow.channel.uid
}
connectedCallback() {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/base.css';
this.component.appendChild(link);
this.container = document.createElement('div'); set value(value) {
this.container.classList.add('chat-input'); this._value = value || "";
this.container.innerHTML = ` this.textarea.value = this._value;
<textarea placeholder="Type a message..." rows="2"></textarea> }
<upload-button></upload-button> get allAutoCompletions() {
`; return Object.assign({},this.autoCompletions,this.hiddenCompletions)
this.textBox = this.container.querySelector('textarea'); }
this.uploadButton = this.container.querySelector('upload-button'); resolveAutoComplete() {
this.uploadButton.chatInput = this let count = 0;
this.textBox.addEventListener('input', (e) => { let value = null;
this.dispatchEvent(new CustomEvent('input', { detail: e.target.value, bubbles: true }));
const message = e.target.value; Object.keys(this.allAutoCompletions).forEach((key) => {
const button = this.container.querySelector('button'); if (key.startsWith(this.value.split(" ")[0])) {
button.disabled = !message; count++;
value = key;
}
});
if (count == 1) return value;
return null;
}
isActive() {
return document.activeElement === this.textarea;
}
focus() {
this.textarea.focus();
}
getAuthors(){
let authors = []
for (let i = 0; i < this.users.length; i++) {
authors.push(this.users[i].username)
authors.push(this.users[i].nick)
}
return authors
}
extractMentions(text) {
const regex = /@([a-zA-Z0-9_-]+)/g;
const mentions = [];
let match;
while ((match = regex.exec(text)) !== null) {
mentions.push(match[1]);
}
return mentions;
}
matchMentionsToAuthors(mentions, authors) {
return mentions.map(mention => {
let closestAuthor = null;
let minDistance = Infinity;
const lowerMention = mention.toLowerCase();
authors.forEach(author => {
const lowerAuthor = author.toLowerCase();
let distance = this.levenshteinDistance(lowerMention, lowerAuthor);
if(!this.isSubsequence(lowerMention,lowerAuthor)) {
distance += 10
}
if (distance < minDistance) {
minDistance = distance;
closestAuthor = author;
}
}); });
this.textBox.addEventListener('change', (e) => { return { mention, closestAuthor, distance: minDistance };
e.preventDefault(); });
this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true })); }
console.error(e.target.value); levenshteinDistance(a, b) {
const matrix = [];
// Initialize the first row and column
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
// Fill in the matrix
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // Deletion
matrix[i][j - 1] + 1, // Insertion
matrix[i - 1][j - 1] + 1 // Substitution
);
}
}
}
return matrix[b.length][a.length];
}
replaceMentionsWithAuthors(text) {
const authors = this.getAuthors();
const mentions = this.extractMentions(text);
const matches = this.matchMentionsToAuthors(mentions, authors);
let updatedText = text;
matches.forEach(({ mention, closestAuthor }) => {
const mentionRegex = new RegExp(`@${mention}`, 'g');
updatedText = updatedText.replace(mentionRegex, `@${closestAuthor}`);
});
return updatedText;
}
async connectedCallback() {
this.user = null
app.rpc.getUser(null).then((user) => {
this.user=user
})
const me = this;
this.liveType = this.getAttribute("live-type") === "true";
this.liveTypeInterval =
parseInt(this.getAttribute("live-type-interval")) || 6;
this.channelUid = this.getAttribute("channel");
app.rpc.getRecentUsers(this.channelUid).then(users=>{
this.users = users
})
this.messageUid = null;
this.classList.add("chat-input");
this.textarea.setAttribute("placeholder", "Type a message...");
this.textarea.setAttribute("rows", "2");
this.appendChild(this.textarea);
this.uploadButton = document.createElement("upload-button");
this.uploadButton.setAttribute("channel", this.channelUid);
this.uploadButton.addEventListener("upload", (e) => {
this.dispatchEvent(new CustomEvent("upload", e));
});
this.uploadButton.addEventListener("uploaded", (e) => {
this.dispatchEvent(new CustomEvent("uploaded", e));
}); });
this.textBox.addEventListener('keydown', (e) => { this.appendChild(this.uploadButton);
if (e.key === 'Enter' && !e.shiftKey) {
this.textarea.addEventListener("keyup", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
this.value = "";
e.target.value = "";
return;
}
this.value = e.target.value;
this.changed = true;
this.update();
});
this.textarea.addEventListener("keydown", (e) => {
this.value = e.target.value;
let autoCompletion = null;
if (e.key === "Tab") {
e.preventDefault(); e.preventDefault();
const message = e.target.value.trim(); autoCompletion = this.resolveAutoComplete();
if (!message) return; if (autoCompletion) {
this.dispatchEvent(new CustomEvent('submit', { detail: message, bubbles: true })); e.target.value = autoCompletion;
e.target.value = ''; this.value = autoCompletion;
return;
}
}
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
const message = me.replaceMentionsWithAuthors(this.value);
e.target.value = "";
if (!message) {
return;
}
let autoCompletionHandler = this.allAutoCompletions[this.value.split(" ")[0]];
if (autoCompletionHandler) {
autoCompletionHandler();
this.value = "";
this.previousValue = "";
e.target.value = "";
return;
}
this.updateMessage()
app.rpc.finalizeMessage(this.messageUid)
this.value = "";
this.previousValue = "";
this.messageUid = null;
} }
}); });
this.component.appendChild(this.container); this.changeInterval = setInterval(() => {
if (!this.liveType) {
return;
}
if (this.value !== this.previousValue) {
if (
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
this.liveTypeInterval
) {
this.value = "";
this.previousValue = "";
}
this.lastChange = new Date();
}
this.update();
}, 300);
this.addEventListener("upload", (e) => {
this.focus();
});
this.addEventListener("uploaded", function (e) {
let message = "";
e.detail.files.forEach((file) => {
message += `[${file.name}](/channel/attachment/${file.relative_url})`;
});
app.rpc.sendMessage(this.channelUid, message,true);
});
setTimeout(()=>{
this.focus();
},1000)
}
trackSecondsBetweenEvents(event1Time, event2Time) {
const millisecondsDifference = event2Time.getTime() - event1Time.getTime();
return millisecondsDifference / 1000;
}
isSubsequence(s, t) {
let i = 0, j = 0;
while (i < s.length && j < t.length) {
if (s[i] === t[j]) {
i++;
}
j++;
}
return i === s.length;
}
newMessage() {
if (!this.messageUid) {
this.messageUid = "?";
}
this.value = this.replaceMentionsWithAuthors(this.value);
this.sendMessage(this.channelUid, this.value,!this.liveType).then((uid) => {
if (this.liveType) {
this.messageUid = uid;
}
});
}
updateMessage() {
if (this.value[0] == "/") {
return false;
}
if (!this.messageUid) {
this.newMessage();
return false;
}
if (this.messageUid === "?") {
return false;
}
if (
typeof app !== "undefined" &&
app.rpc &&
typeof app.rpc.updateMessageText === "function"
) {
app.rpc.updateMessageText(this.messageUid, this.replaceMentionsWithAuthors(this.value));
}
}
updateStatus() {
if (this.liveType) {
return;
}
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) {
this.lastUpdateEvent = new Date();
if (
typeof app !== "undefined" &&
app.rpc &&
typeof app.rpc.set_typing === "function"
) {
app.rpc.set_typing(this.channelUid, this.user.color);
}
}
}
update() {
const expired =
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
this.liveTypeInterval;
const changed = this.value !== this.previousValue;
if (changed || expired) {
this.lastChange = new Date();
this.updateStatus();
}
this.previousValue = this.value;
if (this.liveType && expired) {
this.value = "";
this.previousValue = "";
this.messageUid = null;
return;
}
if (changed) {
if (this.liveType) {
this.updateMessage();
}
}
}
async sendMessage(channelUid, value,is_final) {
if (!value.trim()) {
return null;
}
return await app.rpc.sendMessage(channelUid, value,is_final);
} }
} }
customElements.define('chat-input', ChatInputElement); customElements.define("chat-input", ChatInputComponent);
+58 -58
View File
@@ -6,77 +6,77 @@
// The MIT License (MIT) // The MIT License (MIT)
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// //
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class ChatWindowElement extends HTMLElement { class ChatWindowElement extends HTMLElement {
receivedHistory = false; receivedHistory = false;
channel = null channel = null;
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.component = document.createElement('section'); this.component = document.createElement("section");
this.app = app; this.app = app;
this.shadowRoot.appendChild(this.component); this.shadowRoot.appendChild(this.component);
} }
get user() { get user() {
return this.app.user; return this.app.user;
} }
async connectedCallback() { async connectedCallback() {
const link = document.createElement('link'); const link = document.createElement("link");
link.rel = 'stylesheet'; link.rel = "stylesheet";
link.href = '/base.css'; link.href = "/base.css";
this.component.appendChild(link); this.component.appendChild(link);
this.component.classList.add("chat-area"); this.component.classList.add("chat-area");
this.container = document.createElement("section");
this.container.classList.add("chat-area", "chat-window");
const chatHeader = document.createElement("div"); this.container = document.createElement("section");
chatHeader.classList.add("chat-header"); this.container.classList.add("chat-area", "chat-window");
const chatTitle = document.createElement('h2'); const chatHeader = document.createElement("div");
chatTitle.classList.add("chat-title"); chatHeader.classList.add("chat-header");
chatTitle.classList.add("no-select");
chatTitle.innerText = "Loading...";
chatHeader.appendChild(chatTitle);
this.container.appendChild(chatHeader);
const channels = await app.rpc.getChannels(); const chatTitle = document.createElement("h2");
const channel = channels[0]; chatTitle.classList.add("chat-title");
this.channel = channel; chatTitle.classList.add("no-select");
chatTitle.innerText = channel.name; chatTitle.innerText = "Loading...";
chatHeader.appendChild(chatTitle);
this.container.appendChild(chatHeader);
const channelElement = document.createElement('message-list'); const channels = await app.rpc.getChannels();
channelElement.setAttribute("channel", channel.uid); const channel = channels[0];
this.container.appendChild(channelElement); this.channel = channel;
chatTitle.innerText = channel.name;
const chatInput = document.createElement('chat-input'); const channelElement = document.createElement("message-list");
chatInput.chatWindow = this; channelElement.setAttribute("channel", channel.uid);
chatInput.addEventListener("submit", (e) => { this.container.appendChild(channelElement);
app.rpc.sendMessage(channel.uid, e.detail);
});
this.container.appendChild(chatInput);
this.component.appendChild(this.container); const chatInput = document.createElement("chat-input");
chatInput.chatWindow = this;
chatInput.addEventListener("submit", (e) => {
app.rpc.sendMessage(channel.uid, e.detail);
});
this.container.appendChild(chatInput);
const messages = await app.rpc.getMessages(channel.uid); this.component.appendChild(this.container);
messages.forEach(message => {
if (!message['user_nick']) return;
channelElement.addMessage(message);
});
const me = this; const messages = await app.rpc.getMessages(channel.uid);
channelElement.addEventListener("message", (message) => { messages.forEach((message) => {
if (me.user.uid !== message.detail.user_uid) app.playSound(0); if (!message["user_nick"]) return;
channelElement.addMessage(message);
message.detail.element.scrollIntoView({"block": "end"}); });
});
} const me = this;
channelElement.addEventListener("message", (message) => {
if (me.user.uid !== message.detail.user_uid) app.playSound(0);
message.detail.element.scrollIntoView({ block: "end" });
});
}
} }
customElements.define('chat-window', ChatWindowElement); customElements.define("chat-window", ChatWindowElement);
+150
View File
@@ -0,0 +1,150 @@
class DumbTerminal extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
:host {
--terminal-bg: #111;
--terminal-fg: #0f0;
--terminal-accent: #0ff;
--terminal-font: monospace;
display: block;
background: var(--terminal-bg);
color: var(--terminal-fg);
font-family: var(--terminal-font);
padding: 1rem;
border-radius: 8px;
overflow-y: auto;
max-height: 500px;
}
.output {
white-space: pre-wrap;
margin-bottom: 1em;
}
.input-line {
display: flex;
}
.prompt {
color: var(--terminal-accent);
margin-right: 0.5em;
}
input {
background: transparent;
border: none;
color: var(--terminal-fg);
outline: none;
width: 100%;
font-family: inherit;
font-size: inherit;
}
dialog {
border: none;
background: transparent;
}
.dialog-backdrop {
background: rgba(0, 0, 0, 0.8);
padding: 2rem;
}
</style>
<div class="output" id="output"></div>
<div class="input-line">
<span class="prompt">&gt;</span>
<input type="text" id="input" autocomplete="off" autofocus />
</div>
`;
this.outputEl = this.shadowRoot.getElementById("output");
this.inputEl = this.shadowRoot.getElementById("input");
this.history = [];
this.historyIndex = -1;
this.inputEl.addEventListener("keydown", (e) => this.onKeyDown(e));
}
onKeyDown(event) {
const value = this.inputEl.value;
switch (event.key) {
case "Enter":
this.executeCommand(value);
this.history.push(value);
this.historyIndex = this.history.length;
this.inputEl.value = "";
break;
case "ArrowUp":
if (this.historyIndex > 0) {
this.historyIndex--;
this.inputEl.value = this.history[this.historyIndex];
}
break;
case "ArrowDown":
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++;
this.inputEl.value = this.history[this.historyIndex];
} else {
this.historyIndex = this.history.length;
this.inputEl.value = "";
}
break;
}
}
executeCommand(command) {
const outputLine = document.createElement("div");
outputLine.textContent = `> ${command}`;
this.outputEl.appendChild(outputLine);
const resultLine = document.createElement("div");
resultLine.textContent = this.mockExecute(command);
this.outputEl.appendChild(resultLine);
this.outputEl.scrollTop = this.outputEl.scrollHeight;
}
mockExecute(command) {
switch (command.trim()) {
case "help":
return "Available commands: help, clear, date";
case "date":
return new Date().toString();
case "clear":
this.outputEl.innerHTML = "";
return "";
default:
return `Unknown command: ${command}`;
}
}
/**
* Static method to create a modal dialog with the terminal
* @returns {HTMLDialogElement}
*/
static createModal() {
const dialog = document.createElement("dialog");
dialog.innerHTML = `
<div class="dialog-backdrop">
<web-terminal></web-terminal>
</div>
`;
document.body.appendChild(dialog);
dialog.showModal();
return dialog;
}
}
customElements.define("web-terminal", WebTerminal);
+12 -13
View File
@@ -1,16 +1,15 @@
export class EventHandler { export class EventHandler {
constructor() { constructor() {
this.subscribers = {}; this.subscribers = {};
} }
addEventListener(type, handler) { addEventListener(type, handler) {
if (!this.subscribers[type]) this.subscribers[type] = []; if (!this.subscribers[type]) this.subscribers[type] = [];
this.subscribers[type].push(handler); this.subscribers[type].push(handler);
} }
emit(type, ...data) { emit(type, ...data) {
if (this.subscribers[type]) this.subscribers[type].forEach(handler => handler(...data)); if (this.subscribers[type])
} this.subscribers[type].forEach((handler) => handler(...data));
} }
}
+36 -38
View File
@@ -2,28 +2,25 @@
// This JavaScript class defines a custom HTML element <fancy-button>, which creates a styled, clickable button element with customizable size, text, and URL redirect functionality. // This JavaScript class defines a custom HTML element <fancy-button>, which creates a styled, clickable button element with customizable size, text, and URL redirect functionality.
// MIT License // MIT License
class FancyButton extends HTMLElement { class FancyButton extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.url = null; this.url = null;
this.type = "button"; this.type = "button";
this.value = null; this.value = null;
} }
connectedCallback() { connectedCallback() {
this.container = document.createElement('span'); this.container = document.createElement("span");
let size = this.getAttribute('size'); let size = this.getAttribute("size");
console.info({ GG: size }); console.info({ GG: size });
size = size === 'auto' ? '1%' : '33%'; size = size === "auto" ? "1%" : "33%";
this.styleElement = document.createElement("style"); this.styleElement = document.createElement("style");
this.styleElement.innerHTML = ` this.styleElement.innerHTML = `
:root { :root {
width: 100%; width: 100%;
--width: 100%; --width: 100%;
@@ -49,29 +46,30 @@ class FancyButton extends HTMLElement {
} }
`; `;
this.container.appendChild(this.styleElement); this.container.appendChild(this.styleElement);
this.buttonElement = document.createElement('button'); this.buttonElement = document.createElement("button");
this.container.appendChild(this.buttonElement); this.container.appendChild(this.buttonElement);
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
this.url = this.getAttribute('url'); this.url = this.getAttribute("url");
this.value = this.getAttribute('value'); this.value = this.getAttribute("value");
this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text"))); this.buttonElement.appendChild(
this.buttonElement.addEventListener("click", () => { document.createTextNode(this.getAttribute("text")),
if(this.url == 'submit'){ );
this.closest('form').submit() this.buttonElement.addEventListener("click", () => {
return if (this.url == "submit") {
} this.closest("form").submit();
return;
if (this.url === "/back" || this.url === "/back/") { }
window.history.back();
} else if (this.url) { if (this.url === "/back" || this.url === "/back/") {
window.location = this.url; window.history.back();
} } else if (this.url) {
}); window.location = this.url;
} }
});
}
} }
customElements.define("fancy-button", FancyButton); customElements.define("fancy-button", FancyButton);
+41
View File
@@ -0,0 +1,41 @@
.file-manager {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 16px;
padding: 20px;
background: #111;
color: #ddd;
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
border-radius: 8px;
}
.file-tile {
background-color: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
overflow: hidden;
text-align: center;
padding: 10px;
transition: transform 0.2s;
}
.file-tile:hover {
transform: translateY(-5px);
}
.file-icon {
font-size: 40px;
margin-bottom: 10px;
color: #888;
}
.file-name {
font-size: 14px;
overflow-wrap: break-word;
}
.file-tile img {
max-width: 80%;
height: auto;
margin-bottom: 10px;
border-radius: 4px;
}
+116
View File
@@ -0,0 +1,116 @@
/* A <file-browser> custom element that talks to /api/files */
class FileBrowser extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.path = ""; // current virtual path ("" = ROOT)
this.offset = 0; // pagination offset
this.limit = 40; // items per request
}
connectedCallback() {
this.path = this.getAttribute("path") || "";
this.renderShell();
this.load();
}
// ---------- UI scaffolding -------------------------------------------
renderShell() {
this.shadowRoot.innerHTML = `
<style>
:host { display:block; font-family: system-ui, sans-serif; box-sizing: border-box; }
nav { display:flex; flex-wrap:wrap; gap:.5rem; margin:.5rem 0; align-items:center; }
button { padding:.35rem .65rem; border:none; border-radius:4px; background:#f05a28; color:#fff; cursor:pointer; font:inherit; }
button:disabled { background:#999; cursor:not-allowed; }
.crumb { font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:1rem; }
.tile { border:1px solid #f05a28; border-radius:8px; padding:.5rem; background:#000000; text-align:center; cursor:pointer; transition:box-shadow .2s ease; }
.tile:hover { box-shadow:0 2px 8px rgba(0,0,0,.1); }
img.thumb { width:100%; height:90px; object-fit:cover; border-radius:6px; }
.icon { font-size:48px; line-height:90px; }
</style>
<nav>
<button id="up">⬅️ Up</button>
<span class="crumb" id="crumb"></span>
</nav>
<div class="grid" id="grid"></div>
<nav>
<button id="prev">Prev</button>
<button id="next">Next</button>
</nav>
`;
this.shadowRoot
.getElementById("up")
.addEventListener("click", () => this.goUp());
this.shadowRoot.getElementById("prev").addEventListener("click", () => {
if (this.offset > 0) {
this.offset -= this.limit;
this.load();
}
});
this.shadowRoot.getElementById("next").addEventListener("click", () => {
this.offset += this.limit;
this.load();
});
}
// ---------- Networking ----------------------------------------------
async load() {
const r = await fetch(
`/drive.json?path=${encodeURIComponent(this.path)}&offset=${this.offset}&limit=${this.limit}`,
);
if (!r.ok) {
console.error(await r.text());
return;
}
const data = await r.json();
this.renderTiles(data.items);
this.updateNav(data.pagination);
}
// ---------- Rendering -------------------------------------------------
renderTiles(items) {
const grid = this.shadowRoot.getElementById("grid");
grid.innerHTML = "";
items.forEach((item) => {
const tile = document.createElement("div");
tile.className = "tile";
if (item.type === "directory") {
tile.innerHTML = `<div class="icon">đź“‚</div><div>${item.name}</div>`;
tile.addEventListener("click", () => {
this.path = item.path;
this.offset = 0;
this.load();
});
} else {
if (item.mimetype?.startsWith("image/")) {
tile.innerHTML = `<img class="thumb" src="${item.url}" alt="${item.name}"><div>${item.name}</div>`;
} else {
tile.innerHTML = `<div class="icon">đź“„</div><div>${item.name}</div>`;
}
tile.addEventListener("click", () => window.open(item.url, "_blank"));
}
grid.appendChild(tile);
});
}
// ---------- Navigation + pagination ----------------------------------
updateNav({ offset, limit, total }) {
this.shadowRoot.getElementById("crumb").textContent = `/${this.path}`;
this.shadowRoot.getElementById("prev").disabled = offset === 0;
this.shadowRoot.getElementById("next").disabled = offset + limit >= total;
this.shadowRoot.getElementById("up").disabled = this.path === "";
}
goUp() {
if (!this.path) return;
this.path = this.path.split("/").slice(0, -1).join("/");
this.offset = 0;
this.load();
}
}
customElements.define("file-manager", FileBrowser);
+57 -40
View File
@@ -40,7 +40,7 @@ class GenericField extends HTMLElement {
} }
set value(val) { set value(val) {
val = val ?? ''; val = val ?? "";
this.inputElement.value = val; this.inputElement.value = val;
this.inputElement.setAttribute("value", val); this.inputElement.setAttribute("value", val);
} }
@@ -62,9 +62,9 @@ class GenericField extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({mode: 'open'}); this.attachShadow({ mode: "open" });
this.container = document.createElement('div'); this.container = document.createElement("div");
this.styleElement = document.createElement('style'); this.styleElement = document.createElement("style");
this.styleElement.innerHTML = ` this.styleElement.innerHTML = `
h1 { h1 {
@@ -174,7 +174,7 @@ class GenericField extends HTMLElement {
if (this.inputElement == null && this.field) { if (this.inputElement == null && this.field) {
this.inputElement = document.createElement(this.field.tag); this.inputElement = document.createElement(this.field.tag);
if (this.field.tag === 'button' && this.field.value === "submit") { if (this.field.tag === "button" && this.field.value === "submit") {
this.action = this.field.value; this.action = this.field.value;
} }
this.inputElement.name = this.field.name; this.inputElement.name = this.field.name;
@@ -182,26 +182,39 @@ class GenericField extends HTMLElement {
const me = this; const me = this;
this.inputElement.addEventListener("keyup", (e) => { this.inputElement.addEventListener("keyup", (e) => {
if (e.key === 'Enter') { if (e.key === "Enter") {
const event = new CustomEvent("change", {detail: me, bubbles: true}); const event = new CustomEvent("change", {
detail: me,
bubbles: true,
});
me.dispatchEvent(event); me.dispatchEvent(event);
me.dispatchEvent(new Event("submit")); me.dispatchEvent(new Event("submit"));
} else if (me.field.value !== e.target.value) { } else if (me.field.value !== e.target.value) {
const event = new CustomEvent("change", {detail: me, bubbles: true}); const event = new CustomEvent("change", {
detail: me,
bubbles: true,
});
me.dispatchEvent(event); me.dispatchEvent(event);
} }
}); });
this.inputElement.addEventListener("click", (e) => { this.inputElement.addEventListener("click", (e) => {
const event = new CustomEvent("click", {detail: me, bubbles: true}); const event = new CustomEvent("click", { detail: me, bubbles: true });
me.dispatchEvent(event); me.dispatchEvent(event);
}); });
this.inputElement.addEventListener("blur", (e) => { this.inputElement.addEventListener(
const event = new CustomEvent("change", {detail: me, bubbles: true}); "blur",
me.dispatchEvent(event); (e) => {
}, true); const event = new CustomEvent("change", {
detail: me,
bubbles: true,
});
me.dispatchEvent(event);
},
true,
);
this.container.appendChild(this.inputElement); this.container.appendChild(this.inputElement);
} }
@@ -210,8 +223,8 @@ class GenericField extends HTMLElement {
return; return;
} }
this.inputElement.setAttribute("type", this.field.type ?? 'input'); this.inputElement.setAttribute("type", this.field.type ?? "input");
this.inputElement.setAttribute("name", this.field.name ?? ''); this.inputElement.setAttribute("name", this.field.name ?? "");
if (this.field.text != null) { if (this.field.text != null) {
this.inputElement.innerText = this.field.text; this.inputElement.innerText = this.field.text;
@@ -239,14 +252,14 @@ class GenericField extends HTMLElement {
this.inputElement.removeAttribute("required"); this.inputElement.removeAttribute("required");
} }
if (!this.footerElement) { if (!this.footerElement) {
this.footerElement = document.createElement('div'); this.footerElement = document.createElement("div");
this.footerElement.style.clear = 'both'; this.footerElement.style.clear = "both";
this.container.appendChild(this.footerElement); this.container.appendChild(this.footerElement);
} }
} }
} }
customElements.define('generic-field', GenericField); customElements.define("generic-field", GenericField);
class GenericForm extends HTMLElement { class GenericForm extends HTMLElement {
fields = {}; fields = {};
@@ -254,7 +267,7 @@ class GenericForm extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({mode: 'open'}); this.attachShadow({ mode: "open" });
this.styleElement = document.createElement("style"); this.styleElement = document.createElement("style");
this.styleElement.innerHTML = ` this.styleElement.innerHTML = `
@@ -281,27 +294,29 @@ class GenericForm extends HTMLElement {
} }
}`; }`;
this.container = document.createElement('div'); this.container = document.createElement("div");
this.container.appendChild(this.styleElement); this.container.appendChild(this.styleElement);
this.container.classList.add("generic-form-container"); this.container.classList.add("generic-form-container");
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
} }
connectedCallback() { connectedCallback() {
const preloadedForm = this.getAttribute('preloaded-structure'); const preloadedForm = this.getAttribute("preloaded-structure");
if (preloadedForm) { if (preloadedForm) {
try { try {
const form = JSON.parse(preloadedForm); const form = JSON.parse(preloadedForm);
this.constructForm(form) this.constructForm(form);
} catch (error) { } catch (error) {
console.error(error, preloadedForm); console.error(error, preloadedForm);
} }
} }
const url = this.getAttribute('url'); const url = this.getAttribute("url");
if (url) { if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get"); const fullUrl = url.startsWith("/")
? window.location.origin + url
: new URL(window.location.origin + "/http-get");
if (!url.startsWith("/")) { if (!url.startsWith("/")) {
fullUrl.searchParams.set('url', url); fullUrl.searchParams.set("url", url);
} }
this.loadForm(fullUrl.toString()); this.loadForm(fullUrl.toString());
} else { } else {
@@ -318,10 +333,10 @@ class GenericForm extends HTMLElement {
let hasAutoFocus = Object.keys(this.fields).length !== 0; let hasAutoFocus = Object.keys(this.fields).length !== 0;
fields.sort((a, b) => a.index - b.index); fields.sort((a, b) => a.index - b.index);
fields.forEach(field => { fields.forEach((field) => {
const updatingField = field.name in this.fields const updatingField = field.name in this.fields;
this.fields[field.name] ??= document.createElement('generic-field'); this.fields[field.name] ??= document.createElement("generic-field");
const fieldElement = this.fields[field.name]; const fieldElement = this.fields[field.name];
@@ -362,7 +377,7 @@ class GenericForm extends HTMLElement {
window.location.pathname = saveResult.redirect_url; window.location.pathname = saveResult.redirect_url;
} }
} }
}) });
} }
}); });
} catch (error) { } catch (error) {
@@ -374,7 +389,9 @@ class GenericForm extends HTMLElement {
try { try {
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`); throw new Error(
`Failed to fetch: ${response.status} ${response.statusText}`,
);
} }
await this.constructForm(await response.json()); await this.constructForm(await response.json());
@@ -387,15 +404,15 @@ class GenericForm extends HTMLElement {
const url = this.getAttribute("url"); const url = this.getAttribute("url");
let response = await fetch(url, { let response = await fetch(url, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify({"action": "validate", "form": this.form}) body: JSON.stringify({ action: "validate", form: this.form }),
}); });
const form = await response.json(); const form = await response.json();
Object.values(form.fields).forEach(field => { Object.values(form.fields).forEach((field) => {
if (!this.form.fields[field.name]) { if (!this.form.fields[field.name]) {
return; return;
} }
@@ -409,23 +426,23 @@ class GenericForm extends HTMLElement {
this.fields[field.name].setAttribute("field", field); this.fields[field.name].setAttribute("field", field);
this.fields[field.name].updateAttributes(); this.fields[field.name].updateAttributes();
}); });
Object.values(form.fields).forEach(field => { Object.values(form.fields).forEach((field) => {
this.fields[field.name].setErrors(field.errors); this.fields[field.name].setErrors(field.errors);
}); });
return form['is_valid']; return form["is_valid"];
} }
async submit() { async submit() {
const url = this.getAttribute("url"); const url = this.getAttribute("url");
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify({"action": "submit", "form": this.form}) body: JSON.stringify({ action: "submit", form: this.form }),
}); });
return await response.json(); return await response.json();
} }
} }
customElements.define('generic-form', GenericForm); customElements.define("generic-form", GenericForm);
+41 -39
View File
@@ -7,48 +7,50 @@
// MIT License // MIT License
class HTMLFrame extends HTMLElement { class HTMLFrame extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.container = document.createElement('div'); this.container = document.createElement("div");
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
} }
connectedCallback() { connectedCallback() {
this.container.classList.add("html_frame"); this.container.classList.add("html_frame");
let url = this.getAttribute('url'); let url = this.getAttribute("url");
if (!url.startsWith("https")) { if (!url.startsWith("https")) {
url = "https://" + url; url = "https://" + url;
}
if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get");
if (!url.startsWith("/")) {
fullUrl.searchParams.set('url', url);
}
this.loadAndRender(fullUrl.toString());
} else {
this.container.textContent = "No source URL!";
}
} }
if (url) {
const fullUrl = url.startsWith("/")
? window.location.origin + url
: new URL(window.location.origin + "/http-get");
if (!url.startsWith("/")) {
fullUrl.searchParams.set("url", url);
}
this.loadAndRender(fullUrl.toString());
} else {
this.container.textContent = "No source URL!";
}
}
async loadAndRender(url) { async loadAndRender(url) {
try { try {
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error(`Error: ${response.status} ${response.statusText}`); throw new Error(`Error: ${response.status} ${response.statusText}`);
} }
const html = await response.text(); const html = await response.text();
if (url.endsWith(".md")) { if (url.endsWith(".md")) {
const markdownElement = document.createElement('div'); const markdownElement = document.createElement("div");
markdownElement.innerHTML = html; markdownElement.innerHTML = html;
this.outerHTML = html; this.outerHTML = html;
} else { } else {
this.container.innerHTML = html; this.container.innerHTML = html;
} }
} catch (error) { } catch (error) {
this.container.textContent = `Error: ${error.message}`; this.container.textContent = `Error: ${error.message}`;
}
} }
}
} }
customElements.define('html-frame', HTMLFrame); customElements.define("html-frame", HTMLFrame);
+9 -9
View File
@@ -12,22 +12,22 @@
class HTMLFrame extends HTMLElement { class HTMLFrame extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.container = document.createElement('div'); this.container = document.createElement("div");
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
} }
connectedCallback() { connectedCallback() {
this.container.classList.add('html_frame'); this.container.classList.add("html_frame");
const url = this.getAttribute('url'); const url = this.getAttribute("url");
if (url) { if (url) {
const fullUrl = url.startsWith('/') const fullUrl = url.startsWith("/")
? window.location.origin + url ? window.location.origin + url
: new URL(window.location.origin + '/http-get'); : new URL(window.location.origin + "/http-get");
if (!url.startsWith('/')) fullUrl.searchParams.set('url', url); if (!url.startsWith("/")) fullUrl.searchParams.set("url", url);
this.loadAndRender(fullUrl.toString()); this.loadAndRender(fullUrl.toString());
} else { } else {
this.container.textContent = 'No source URL!'; this.container.textContent = "No source URL!";
} }
} }
@@ -45,4 +45,4 @@ class HTMLFrame extends HTMLElement {
} }
} }
customElements.define('markdown-frame', HTMLFrame); customElements.define("markdown-frame", HTMLFrame);
+98 -100
View File
@@ -4,7 +4,6 @@
// No external libraries or dependencies are used other than standard web components. // No external libraries or dependencies are used other than standard web components.
// MIT License // MIT License
// //
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
@@ -13,20 +12,19 @@
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class TileGridElement extends HTMLElement { class TileGridElement extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.gridId = this.getAttribute('grid'); this.gridId = this.getAttribute("grid");
this.component = document.createElement('div'); this.component = document.createElement("div");
this.shadowRoot.appendChild(this.component); this.shadowRoot.appendChild(this.component);
} }
connectedCallback() { connectedCallback() {
console.log('connected'); console.log("connected");
this.styleElement = document.createElement('style'); this.styleElement = document.createElement("style");
this.styleElement.textContent = ` this.styleElement.textContent = `
.grid { .grid {
padding: 10px; padding: 10px;
display: flex; display: flex;
@@ -47,53 +45,53 @@ class TileGridElement extends HTMLElement {
transform: scale(1.1); transform: scale(1.1);
} }
`; `;
this.component.appendChild(this.styleElement); this.component.appendChild(this.styleElement);
this.container = document.createElement('div'); this.container = document.createElement("div");
this.container.classList.add('gallery'); this.container.classList.add("gallery");
this.component.appendChild(this.container); this.component.appendChild(this.container);
} }
addImage(src) { addImage(src) {
const item = document.createElement('img'); const item = document.createElement("img");
item.src = src; item.src = src;
item.classList.add('tile'); item.classList.add("tile");
item.style.width = '100px'; item.style.width = "100px";
item.style.height = '100px'; item.style.height = "100px";
this.container.appendChild(item); this.container.appendChild(item);
} }
addImages(srcs) { addImages(srcs) {
srcs.forEach(src => this.addImage(src)); srcs.forEach((src) => this.addImage(src));
} }
addElement(element) { addElement(element) {
element.classList.add('tile'); element.classList.add("tile");
this.container.appendChild(element); this.container.appendChild(element);
} }
} }
class UploadButton extends HTMLElement { class UploadButton extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.component = document.createElement('div'); this.component = document.createElement("div");
this.shadowRoot.appendChild(this.component); this.shadowRoot.appendChild(this.component);
window.u = this; window.u = this;
} }
get gridSelector() { get gridSelector() {
return this.getAttribute('grid'); return this.getAttribute("grid");
} }
grid = null; grid = null;
addImages(urls) { addImages(urls) {
this.grid.addImages(urls); this.grid.addImages(urls);
} }
connectedCallback() { connectedCallback() {
console.log('connected'); console.log("connected");
this.styleElement = document.createElement('style'); this.styleElement = document.createElement("style");
this.styleElement.textContent = ` this.styleElement.textContent = `
.upload-button { .upload-button {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -115,61 +113,61 @@ class UploadButton extends HTMLElement {
background-color: #999; background-color: #999;
} }
`; `;
this.shadowRoot.appendChild(this.styleElement); this.shadowRoot.appendChild(this.styleElement);
this.container = document.createElement('div'); this.container = document.createElement("div");
this.container.classList.add('upload-button'); this.container.classList.add("upload-button");
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
const input = document.createElement('input'); const input = document.createElement("input");
input.type = 'file'; input.type = "file";
input.accept = 'image/*'; input.accept = "image/*";
input.multiple = true; input.multiple = true;
input.addEventListener('change', (e) => { input.addEventListener("change", (e) => {
const files = e.target.files; const files = e.target.files;
const urls = []; const urls = [];
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
urls.push(e.target.result); urls.push(e.target.result);
if (urls.length === files.length) { if (urls.length === files.length) {
this.addImages(urls); this.addImages(urls);
} }
}; };
reader.readAsDataURL(files[i]); reader.readAsDataURL(files[i]);
} }
}); });
const label = document.createElement('label'); const label = document.createElement("label");
label.textContent = 'Upload Images'; label.textContent = "Upload Images";
label.appendChild(input); label.appendChild(input);
this.container.appendChild(label); this.container.appendChild(label);
} }
} }
customElements.define('upload-button', UploadButton); customElements.define("upload-button", UploadButton);
customElements.define('tile-grid', TileGridElement); customElements.define("tile-grid", TileGridElement);
class MeniaUploadElement extends HTMLElement { class MeniaUploadElement extends HTMLElement {
constructor(){ constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.component = document.createElement("div"); this.component = document.createElement("div");
alert('aaaa'); alert("aaaa");
this.shadowRoot.appendChild(this.component); this.shadowRoot.appendChild(this.component);
} }
connectedCallback() { connectedCallback() {
this.container = document.createElement("div"); this.container = document.createElement("div");
this.component.style.height = '100%'; this.component.style.height = "100%";
this.component.style.backgroundColor = 'blue'; this.component.style.backgroundColor = "blue";
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
this.tileElement = document.createElement("tile-grid"); this.tileElement = document.createElement("tile-grid");
this.tileElement.style.backgroundColor = 'red'; this.tileElement.style.backgroundColor = "red";
this.tileElement.style.height = '100%'; this.tileElement.style.height = "100%";
this.component.appendChild(this.tileElement); this.component.appendChild(this.tileElement);
this.uploadButton = document.createElement('upload-button'); this.uploadButton = document.createElement("upload-button");
this.component.appendChild(this.uploadButton); this.component.appendChild(this.uploadButton);
} }
} }
customElements.define('menia-upload', MeniaUploadElement); customElements.define("menia-upload", MeniaUploadElement);
+16 -17
View File
@@ -3,7 +3,7 @@
// This JavaScript source code defines a custom HTML element named "message-list-manager" to manage a list of message lists for different channels obtained asynchronously. // This JavaScript source code defines a custom HTML element named "message-list-manager" to manage a list of message lists for different channels obtained asynchronously.
// //
// //
// MIT License // MIT License
// Permission is hereby granted, free of charge, to any person obtaining a copy // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -22,23 +22,22 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
class MessageListManagerElement extends HTMLElement { class MessageListManagerElement extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
this.container = document.createElement("div"); this.container = document.createElement("div");
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
} }
async connectedCallback() { async connectedCallback() {
const channels = await app.rpc.getChannels(); const channels = await app.rpc.getChannels();
channels.forEach(channel => { channels.forEach((channel) => {
const messageList = document.createElement("message-list"); const messageList = document.createElement("message-list");
messageList.setAttribute("channel", channel.uid); messageList.setAttribute("channel", channel.uid);
this.container.appendChild(messageList); this.container.appendChild(messageList);
}); });
} }
} }
customElements.define("message-list-manager", MessageListManagerElement); customElements.define("message-list-manager", MessageListManagerElement);
+93 -156
View File
@@ -5,166 +5,103 @@
// 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. // 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. // 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";
class MessageList extends HTMLElement {
constructor() {
super();
app.ws.addEventListener("update_message_text", (data) => {
this.updateMessageText(data.uid, data);
});
app.ws.addEventListener("set_typing", (data) => {
this.triggerGlow(data.user_uid,data.color);
});
class MessageListElement extends HTMLElement { this.items = [];
static get observedAttributes() { }
return ["messages"];
connectedCallback() {
const messagesContainer = this
messagesContainer.addEventListener('click', (e) => {
if (e.target.tagName !== 'IMG' || e.target.classList.contains('avatar-img')) return;
const img = e.target;
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;'
const fullImg = document.createElement('img');
const urlObj = new URL(img.src); urlObj.search = '';
fullImg.src = urlObj.toString();
fullImg.alt = img.alt;
fullImg.style.maxWidth = '90%';
fullImg.style.maxHeight = '90%';
overlay.appendChild(fullImg);
document.body.appendChild(overlay);
overlay.addEventListener('click', () => document.body.removeChild(overlay));
})
}
isElementVisible(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
isScrolledToBottom() {
return this.isElementVisible(this.querySelector(".message-list-bottom"));
}
scrollToBottom(force) {
this.scrollTop = this.scrollHeight;
this.querySelector(".message-list-bottom").scrollIntoView();
setTimeout(() => {
this.scrollTop = this.scrollHeight;
this.querySelector(".message-list-bottom").scrollIntoView();
},200)
}
updateMessageText(uid, message) {
const messageDiv = this.querySelector('div[data-uid="' + uid + '"]');
if (!messageDiv) {
return;
} }
const scrollToBottom = this.isScrolledToBottom();
messages = []; const receivedHtml = document.createElement("div");
room = null; receivedHtml.innerHTML = message.html;
url = null; const html = receivedHtml.querySelector(".text").innerHTML;
container = null; const textElement = messageDiv.querySelector(".text");
messageEventSchedule = null; textElement.innerHTML = html;
observer = null; textElement.style.display = message.text == "" ? "none" : "block";
if(scrollToBottom)
constructor() { this.scrollToBottom(true)
super(); }
this.attachShadow({ mode: 'open' }); triggerGlow(uid,color) {
this.component = document.createElement('div'); app.starField.glowColor(color)
this.shadowRoot.appendChild(this.component); let lastElement = null;
this.querySelectorAll(".avatar").forEach((el) => {
const div = el.closest("a");
if (el.href.indexOf(uid) != -1) {
lastElement = el;
}
});
if (lastElement) {
lastElement.classList.add("glow");
setTimeout(() => {
lastElement.classList.remove("glow");
}, 1000);
} }
}
linkifyText(text) { set data(items) {
const urlRegex = /https?:\/\/[^\s]+/g; this.items = items;
return text.replace(urlRegex, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`); this.render();
} }
render() {
this.innerHTML = "";
timeAgo(date1, date2) { //this.insertAdjacentHTML("beforeend", html);
const diffMs = Math.abs(date2 - date1); }
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
if (days) {
return `${days} ${days > 1 ? 'days' : 'day'} ago`;
}
if (hours) {
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`;
}
if (minutes) {
return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`;
}
return 'just now';
}
timeDescription(isoDate) {
const date = new Date(isoDate);
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
return timeStr;
}
createElement(message) {
const element = document.createElement("div");
element.dataset.uid = message.uid;
element.dataset.color = message.color;
element.dataset.channel_uid = message.channel_uid;
element.dataset.user_nick = message.user_nick;
element.dataset.created_at = message.created_at;
element.dataset.user_uid = message.user_uid;
element.dataset.message = message.message;
element.classList.add("message");
if (!this.messages.length || this.messages[this.messages.length - 1].user_uid != message.user_uid) {
element.classList.add("switch-user");
}
const avatar = document.createElement("div");
avatar.classList.add("avatar");
avatar.classList.add("no-select");
avatar.style.backgroundColor = message.color;
avatar.style.color = "black";
avatar.innerText = message.user_nick[0];
const messageContent = document.createElement("div");
messageContent.classList.add("message-content");
const author = document.createElement("div");
author.classList.add("author");
author.style.color = message.color;
author.textContent = message.user_nick;
const text = document.createElement("div");
text.classList.add("text");
if (message.html) text.innerHTML = message.html;
const time = document.createElement("div");
time.classList.add("time");
time.dataset.created_at = message.created_at;
time.textContent = this.timeDescription(message.created_at);
messageContent.appendChild(author);
messageContent.appendChild(text);
messageContent.appendChild(time);
element.appendChild(avatar);
element.appendChild(messageContent);
message.element = element;
return element;
}
addMessage(message) {
const obj = new models.Message(
message.uid,
message.channel_uid,
message.user_uid,
message.user_nick,
message.color,
message.message,
message.html,
message.created_at,
message.updated_at
);
const element = this.createElement(obj);
this.messages.push(obj);
this.container.appendChild(element);
this.messageEventSchedule.delay(() => {
this.dispatchEvent(new CustomEvent("message", { detail: obj, bubbles: true }));
});
return obj;
}
scrollBottom() {
this.container.scrollTop = this.container.scrollHeight;
}
connectedCallback() {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/base.css';
this.component.appendChild(link);
this.component.classList.add("chat-messages");
this.container = document.createElement('div');
this.component.appendChild(this.container);
this.messageEventSchedule = new Schedule(500);
this.messages = [];
this.channel_uid = this.getAttribute("channel");
app.addEventListener(this.channel_uid, (data) => {
this.addMessage(data);
});
this.dispatchEvent(new CustomEvent("rendered", { detail: this, bubbles: true }));
this.timeUpdateInterval = setInterval(() => {
this.messages.forEach((message) => {
const newText = this.timeDescription(message.created_at);
if (newText != message.element.innerText) {
message.element.querySelector(".time").innerText = newText;
}
});
}, 30000);
}
} }
customElements.define('message-list', MessageListElement); customElements.define("message-list", MessageList);
+24 -14
View File
@@ -7,20 +7,30 @@
// MIT License // MIT License
class MessageModel { class MessageModel {
constructor(uid, channel_uid, user_uid, user_nick, color, message, html, created_at, updated_at) { constructor(
this.uid = uid uid,
this.message = message channel_uid,
this.html = html user_uid,
this.user_uid = user_uid user_nick,
this.user_nick = user_nick color,
this.color = color message,
this.channel_uid = channel_uid html,
this.created_at = created_at created_at,
this.updated_at = updated_at updated_at,
this.element = null ) {
} this.uid = uid;
this.message = message;
this.html = html;
this.user_uid = user_uid;
this.user_nick = user_nick;
this.color = color;
this.channel_uid = channel_uid;
this.created_at = created_at;
this.updated_at = updated_at;
this.element = null;
}
} }
const models = { const models = {
Message: MessageModel Message: MessageModel,
} };
View File
+32 -28
View File
@@ -1,30 +1,34 @@
this.onpush = (event) => { this.onpush = (event) => {
console.log(event.data); console.log(event.data);
// From here we can write the data to IndexedDB, send it to any open // From here we can write the data to IndexedDB, send it to any open
// windows, display a notification, etc. // windows, display a notification, etc.
}; };
navigator.serviceWorker navigator.serviceWorker
.register("/service-worker.js") .register("/service-worker.js")
.then((serviceWorkerRegistration) => { .then((serviceWorkerRegistration) => {
serviceWorkerRegistration.pushManager.subscribe().then( serviceWorkerRegistration.pushManager.subscribe().then(
(pushSubscription) => { (pushSubscription) => {
const subscriptionObject = { const subscriptionObject = {
endpoint: pushSubscription.endpoint, endpoint: pushSubscription.endpoint,
keys: { keys: {
p256dh: pushSubscription.getKey('p256dh'), p256dh: pushSubscription.getKey("p256dh"),
auth: pushSubscription.getKey('auth'), auth: pushSubscription.getKey("auth"),
}, },
encoding: PushManager.supportedContentEncodings, encoding: PushManager.supportedContentEncodings,
/* other app-specific data, such as user identity */ /* other app-specific data, such as user identity */
}; };
console.log(pushSubscription.endpoint, pushSubscription, subscriptionObject); console.log(
// The push subscription details needed by the application pushSubscription.endpoint,
// server are now available, and can be sent to it using, pushSubscription,
// for example, the fetch() API. subscriptionObject,
}, );
(error) => { // The push subscription details needed by the application
console.error(error); // server are now available, and can be sent to it using,
}, // for example, the fetch() API.
); },
}); (error) => {
console.error(error);
},
);
});
+179
View File
@@ -0,0 +1,179 @@
:root {
--star-color: white;
--background-color: black;
}
body.day {
--star-color: #444;
--background-color: #e6f0ff;
}
body.night {
--star-color: white;
--background-color: black;
}
body {
margin: 0;
overflow: hidden;
background-color: var(--background-color);
transition: background-color 0.5s;
}
.star {
position: absolute;
border-radius: 50%;
background-color: var(--star-color);
animation: twinkle 2s infinite ease-in-out;
z-index: -1;
}
@keyframes twinkle {
0%, 100% { opacity: 0.8; transform: scale(1); }
50% { opacity: 1; transform: scale(1.2); }
}
#themeToggle {
position: absolute;
top: 10px;
left: 10px;
padding: 8px 12px;
font-size: 14px;
z-index: 1000;
}
.star.special {
box-shadow: 0 0 10px 3px gold;
transform: scale(1.4);
z-index: 10;
}
.star-tooltip {
position: absolute;
font-size: 12px;
color: white;
font-family: sans-serif;
pointer-events: none;
z-index: 9999;
white-space: nowrap;
text-shadow: 1px 1px 2px black;
display: none;
padding: 2px 6px;
}
.star-popup {
position: absolute;
max-width: 300px;
color: #fff;
font-family: sans-serif;
font-size: 14px;
z-index: 10000;
text-shadow: 1px 1px 3px black;
display: none;
padding: 10px;
border-radius: 12px;
}
.star:hover {
cursor: pointer;
}
.star-popup {
position: absolute;
max-width: 300px;
background: white;
color: black;
padding: 15px;
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
z-index: 10000;
font-family: sans-serif;
font-size: 14px;
display: none;
}
.star-popup h3 {
margin: 0 0 5px;
font-size: 16px;
}
.star-popup button {
margin-top: 10px;
}
.demo-overlay {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 3em;
color: white;
font-family: 'Segoe UI', sans-serif;
font-weight: 300;
text-align: center;
text-shadow: 0 0 20px rgba(0,0,0,0.8);
z-index: 9999;
opacity: 0;
transition: opacity 0.6s ease;
max-width: 80vw;
pointer-events: none;
}
@keyframes demoFadeIn {
from {
opacity: 0;
transform: translate(-50%, -60%) scale(0.95);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes demoPulse {
0% {
box-shadow: 0 0 0 rgba(255, 255, 150, 0);
transform: scale(1);
}
30% {
box-shadow: 0 0 30px 15px rgba(255, 255, 150, 0.9);
transform: scale(1.05);
}
100% {
box-shadow: 0 0 0 rgba(255, 255, 150, 0);
transform: scale(1);
}
}
.demo-highlight {
animation: demoPulse 1.5s ease-out;
font-weight: bold;
position: relative;
z-index: 9999;
}
.star-notify-container {
position: fixed;
top: 50px;
right: 20px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
z-index: 9999;
pointer-events: none;
}
.star-notify {
opacity: 0;
background: transparent;
padding: 5px 10px;
color: white;
font-weight: 300;
text-shadow: 0 0 10px rgba(0,0,0,0.7);
transition: opacity 0.5s ease, transform 0.5s ease;
transform: translateY(-10px);
font-family: 'Segoe UI', sans-serif;
}
+1 -1
View File
@@ -51,4 +51,4 @@ export class Schedule {
me.timeOutCount = 0; me.timeOutCount = 0;
}, this.msDelay); }, this.msDelay);
} }
} }
+11 -10
View File
@@ -1,33 +1,34 @@
async function requestNotificationPermission() { async function requestNotificationPermission() {
const permission = await Notification.requestPermission(); const permission = await Notification.requestPermission();
return permission === 'granted'; return permission === "granted";
} }
// Subscribe to Push Notifications // Subscribe to Push Notifications
async function subscribeUser() { async function subscribeUser() {
const registration = await navigator.serviceWorker.register('/service-worker.js'); const registration =
await navigator.serviceWorker.register("/service-worker.js");
const subscription = await registration.pushManager.subscribe({ const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true, userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY) applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY),
}); });
// Send subscription to your backend // Send subscription to your backend
await fetch('/subscribe', { await fetch("/subscribe", {
method: 'POST', method: "POST",
body: JSON.stringify(subscription), body: JSON.stringify(subscription),
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}); });
} }
// Service Worker (service-worker.js) // Service Worker (service-worker.js)
self.addEventListener('push', event => { self.addEventListener("push", (event) => {
const data = event.data.json(); const data = event.data.json();
self.registration.showNotification(data.title, { self.registration.showNotification(data.title, {
body: data.message, body: data.message,
icon: data.icon icon: data.icon,
}); });
}); });
+131 -117
View File
@@ -1,137 +1,151 @@
import {EventHandler} from "./event-handler.js"; import { EventHandler } from "./event-handler.js";
export class Socket extends EventHandler { export class Socket extends EventHandler {
/** /**
* @type {URL} * @type {URL}
*/ */
url url;
/** /**
* @type {WebSocket|null} * @type {WebSocket|null}
*/ */
ws = null ws = null;
/** /**
* @type {null|PromiseWithResolvers<Socket>&{resolved?:boolean}} * @type {null|PromiseWithResolvers<Socket>&{resolved?:boolean}}
*/ */
connection = null connection = null;
shouldReconnect = true; shouldReconnect = true;
get isConnected() { _debug = false;
return this.ws && this.ws.readyState === WebSocket.OPEN;
get isConnected() {
return this.ws && this.ws.readyState === WebSocket.OPEN;
}
get isConnecting() {
return this.ws && this.ws.readyState === WebSocket.CONNECTING;
}
constructor() {
super();
this.url = new URL("/rpc.ws", window.location.origin);
this.url.protocol = this.url.protocol.replace("http", "ws");
this.connect();
}
connect() {
if (this.ws) {
return this.connection.promise;
} }
get isConnecting() { if (!this.connection || this.connection.resolved) {
return this.ws && this.ws.readyState === WebSocket.CONNECTING; this.connection = Promise.withResolvers();
} }
constructor() { this.ws = new WebSocket(this.url);
super(); this.ws.addEventListener("open", () => {
this.connection.resolved = true;
this.connection.resolve(this);
this.emit("connected");
});
this.url = new URL('/rpc.ws', window.location.origin); this.ws.addEventListener("close", () => {
this.url.protocol = this.url.protocol.replace('http', 'ws'); console.log("Connection closed");
this.disconnect();
this.connect() });
} this.ws.addEventListener("error", (e) => {
console.error("Connection error", e);
connect() { this.disconnect();
if (this.ws) { });
return this.connection.promise; this.ws.addEventListener("message", (e) => {
if (e.data instanceof Blob || e.data instanceof ArrayBuffer) {
console.error("Binary data not supported");
} else {
try {
this.onData(JSON.parse(e.data));
} catch (e) {
console.error("Failed to parse message", e);
} }
}
});
}
if (!this.connection || this.connection.resolved) { onData(data) {
this.connection = Promise.withResolvers() if (data.success !== undefined && !data.success) {
} console.error(data);
this.ws = new WebSocket(this.url);
this.ws.addEventListener("open", () => {
this.connection.resolved = true;
this.connection.resolve(this);
this.emit("connected");
});
this.ws.addEventListener("close", () => {
console.log("Connection closed");
this.disconnect()
})
this.ws.addEventListener("error", (e) => {
console.error("Connection error", e);
this.disconnect()
})
this.ws.addEventListener("message", (e) => {
if (e.data instanceof Blob || e.data instanceof ArrayBuffer) {
console.error("Binary data not supported");
} else {
try {
this.onData(JSON.parse(e.data));
} catch (e) {
console.error("Failed to parse message", e);
}
}
})
} }
if (data.callId) {
this.emit(data.callId, data.data);
onData(data) {
if (data.success !== undefined && !data.success) {
console.error(data);
}
if (data.callId) {
this.emit(data.callId, data.data);
}
if (data.channel_uid) {
this.emit(data.channel_uid, data.data);
this.emit("channel-message", data);
}
} }
if (data.channel_uid) {
disconnect() { this.emit(data.channel_uid, data.data);
this.ws?.close(); if (!data["event"]) this.emit("channel-message", data);
this.ws = null;
if (this.shouldReconnect) setTimeout(() => {
console.log("Reconnecting");
return this.connect();
}, 0);
} }
this.emit("data", data.data);
if (data["event"]) {
_camelToSnake(str) { this.emit(data.event, data.data);
return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
} }
}
get client() { disconnect() {
const me = this; this.ws?.close();
return new Proxy({}, { this.ws = null;
get(_, prop) {
return (...args) => {
const functionName = me._camelToSnake(prop);
return me.call(functionName, ...args);
};
},
});
}
generateCallId() { if (this.shouldReconnect)
return self.crypto.randomUUID(); setTimeout(() => {
} console.log("Reconnecting");
this.emit("reconnecting");
return this.connect();
}, 0);
}
async sendJson(data) { _camelToSnake(str) {
await this.connect().then(api => { return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
api.ws.send(JSON.stringify(data)); }
});
}
async call(method, ...args) { get client() {
const call = { const me = this;
callId: this.generateCallId(), return new Proxy(
method, {},
args, {
}; get(_, prop) {
const me = this return (...args) => {
return new Promise((resolve) => { const functionName = me._camelToSnake(prop);
me.addEventListener(call.callId, data => resolve(data)); if(me._debug){
me.sendJson(call); const call = {}
}); call[functionName] = args
} console.debug(call)
} }
return me.call(functionName, ...args);
};
},
},
);
}
generateCallId() {
return self.crypto.randomUUID();
}
async sendJson(data) {
await this.connect().then((api) => {
api.ws.send(JSON.stringify(data));
});
}
async call(method, ...args) {
const call = {
callId: this.generateCallId(),
method,
args,
};
const me = this;
return new Promise((resolve) => {
me.addEventListener(call.callId, (data) => resolve(data));
me.sendJson(call);
});
}
}
+65 -63
View File
@@ -2,60 +2,62 @@
// This class defines a custom HTML element for an upload button with integrated file upload functionality using XMLHttpRequest. // This class defines a custom HTML element for an upload button with integrated file upload functionality using XMLHttpRequest.
// MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: // MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
class UploadButtonElement extends HTMLElement { class UploadButtonElement extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: "open" });
}
chatInput = null;
async uploadFiles() {
const fileInput = this.container.querySelector(".file-input");
const uploadButton = this.container.querySelector(".upload-button");
if (!fileInput.files.length) {
return;
} }
chatInput = null
async uploadFiles() {
const fileInput = this.container.querySelector('.file-input');
const uploadButton = this.container.querySelector('.upload-button');
if (!fileInput.files.length) { const files = fileInput.files;
return; const formData = new FormData();
} for (let i = 0; i < files.length; i++) {
formData.append("files[]", files[i]);
const files = fileInput.files;
const formData = new FormData();
formData.append('channel_uid', this.channelUid);
for (let i = 0; i < files.length; i++) {
formData.append('files[]', files[i]);
}
const request = new XMLHttpRequest();
request.open('POST', '/drive.bin', true);
request.upload.onprogress = function (event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
uploadButton.innerText = `${Math.round(percentComplete)}%`;
}
};
request.onload = function () {
if (request.status === 200) {
uploadButton.innerHTML = '📤';
} else {
alert('Upload failed');
}
};
request.onerror = function () {
alert('Error while uploading.');
};
request.send(formData);
const uploadEvent = new Event('upload',{});
this.dispatchEvent(uploadEvent);
} }
channelUid = null const request = new XMLHttpRequest();
connectedCallback() {
this.styleElement = document.createElement('style'); request.responseType = "json";
this.styleElement.innerHTML = ` request.open("POST", `/channel/${this.channelUid}/attachment.bin`, true);
request.upload.onprogress = function (event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
uploadButton.innerText = `${Math.round(percentComplete)}%`;
}
};
const me = this;
request.onload = function () {
if (request.status === 200) {
me.dispatchEvent(
new CustomEvent("uploaded", { detail: request.response }),
);
uploadButton.innerHTML = "📤";
} else {
alert("Upload failed");
}
};
request.onerror = function () {
alert("Error while uploading.");
};
request.send(formData);
const uploadEvent = new Event("upload", {});
this.dispatchEvent(uploadEvent);
}
channelUid = null;
connectedCallback() {
this.styleElement = document.createElement("style");
this.styleElement.innerHTML = `
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
display: flex; display: flex;
@@ -96,9 +98,9 @@ class UploadButtonElement extends HTMLElement {
display: none; display: none;
} }
`; `;
this.shadowRoot.appendChild(this.styleElement); this.shadowRoot.appendChild(this.styleElement);
this.container = document.createElement('div'); this.container = document.createElement("div");
this.container.innerHTML = ` this.container.innerHTML = `
<div class="upload-container"> <div class="upload-container">
<button class="upload-button"> <button class="upload-button">
📤 📤
@@ -106,17 +108,17 @@ class UploadButtonElement extends HTMLElement {
<input class="hidden-input file-input" type="file" multiple /> <input class="hidden-input file-input" type="file" multiple />
</div> </div>
`; `;
this.shadowRoot.appendChild(this.container); this.shadowRoot.appendChild(this.container);
this.channelUid = this.getAttribute('channel'); this.channelUid = this.getAttribute("channel");
this.uploadButton = this.container.querySelector('.upload-button'); this.uploadButton = this.container.querySelector(".upload-button");
this.fileInput = this.container.querySelector('.hidden-input'); this.fileInput = this.container.querySelector(".hidden-input");
this.uploadButton.addEventListener('click', () => { this.uploadButton.addEventListener("click", () => {
this.fileInput.click(); this.fileInput.click();
}); });
this.fileInput.addEventListener('change', () => { this.fileInput.addEventListener("change", () => {
this.uploadFiles(); this.uploadFiles();
}); });
} }
} }
customElements.define('upload-button', UploadButtonElement); customElements.define("upload-button", UploadButtonElement);
+28
View File
@@ -0,0 +1,28 @@
.user-list__item {
display: flex;
margin-bottom: 1em;
border: 1px solid #ccc;
padding: 10px;
border-radius: 8px;
}
.user-list__item-avatar {
margin-right: 10px;
border-radius: 50%;
overflow: hidden;
width: 40px;
height: 40px;
display: block;
}
.user-list__item-content {
flex: 1;
}
.user-list__item-name {
font-weight: bold;
}
.user-list__item-text {
margin: 5px 0;
}
.user-list__item-time {
font-size: 0.8em;
color: gray;
}
+59
View File
@@ -0,0 +1,59 @@
class UserList extends HTMLElement {
constructor() {
super();
this.users = [];
}
set data(userArray) {
this.users = userArray;
this.render();
}
formatRelativeTime(timestamp) {
const now = new Date();
const msgTime = new Date(timestamp);
const diffMs = now - msgTime;
const minutes = Math.floor(diffMs / 60000);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${days} day${days > 1 ? "s" : ""} ago`;
} else if (hours > 0) {
return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${hours} hour${hours > 1 ? "s" : ""} ago`;
} else {
return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${minutes} min ago`;
}
}
render() {
this.innerHTML = "";
this.users.forEach((user) => {
const html = `
<div class="user-list__item"
data-uid="${user.uid}"
data-color="${user.color}"
data-user_nick="${user.nick}"
data-created_at="${user.created_at}"
data-user_uid="${user.user_uid}">
<a class="user-list__item-avatar" style="background-color: ${user.color}; color: black;" href="/user/${user.uid}.html">
<img width="40px" height="40px" src="/avatar/${user.uid}.svg" alt="${user.nick}">
</a>
<div class="user-list__item-content">
<div class="user-list__item-name" style="color: ${user.color};">${user.nick}</div>
<div class="user-list__item-time" data-created_at="${user.last_ping}">
<a href="/user/${user.uid}.html">profile</a>
<a href="/channel/${user.uid}.html">dm</a>
</div>
</div>
</div>
`;
this.insertAdjacentHTML("beforeend", html);
});
}
}
customElements.define("user-list", UserList);
+135
View File
@@ -0,0 +1,135 @@
class DatasetWebSocketView:
def __init__(self):
self.ws = None
self.db = dataset.connect('sqlite:///snek.db')
self.setattr(self, "db", self.get)
self.setattr(self, "db", self.set)
)
super()
def format_result(self, result):
try:
return dict(result)
except:
pass
try:
return [dict(row) for row in result]
except:
pass
return result
async def send_str(self, msg):
return await self.ws.send_str(msg)
def get(self, key):
returnl loads(dict(self.db['_kv'].get(key=key)['value']))
def set(self, key, value):
return self.db['_kv'].upsert({'key': key, 'value': json.dumps(value)}, ['key'])
async def handle(self, request):
ws = web.WebSocketResponse()
await ws.prepare(request)
self.ws = ws
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
try:
data = json.loads(msg.data)
call_uid = data.get("call_uid")
method = data.get("method")
table_name = data.get("table")
args = data.get("args", {})
kwargs = data.get("kwargs", {})
function = getattr(self.db, method, None)
if table_name:
function = getattr(self.db[table_name], method, None)
print(method, table_name, args, kwargs,flush=True)
if function:
response = {}
try:
result = function(*args, **kwargs)
print(result)
response['result'] = self.format_result(result)
response["call_uid"] = call_uid
response["success"] = True
except Exception as e:
response["call_uid"] = call_uid
response["success"] = False
response["error"] = str(e)
response["traceback"] = traceback.format_exc()
if call_uid:
await self.send_str(json.dumps(response,default=str))
else:
await self.send_str(json.dumps({"status": "error", "error":"Method not found.","call_uid": call_uid}))
except Exception as e:
await self.send_str(json.dumps({"success": False,"call_uid": call_uid, "error": str(e), "error": str(e), "traceback": traceback.format_exc()},default=str))
elif msg.type == aiohttp.WSMsgType.ERROR:
print('ws connection closed with exception %s' % ws.exception())
return ws
class BroadCastSocketView:
def __init__(self):
self.ws = None
super()
def format_result(self, result):
try:
return dict(result)
except:
pass
try:
return [dict(row) for row in result]
except:
pass
return result
async def send_str(self, msg):
return await self.ws.send_str(msg)
def get(self, key):
returnl loads(dict(self.db['_kv'].get(key=key)['value']))
def set(self, key, value):
return self.db['_kv'].upsert({'key': key, 'value': json.dumps(value)}, ['key'])
async def handle(self, request):
ws = web.WebSocketResponse()
await ws.prepare(request)
self.ws = ws
app = request.app
app['broadcast_clients'].append(ws)
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
print(msg.data)
for client in app['broadcast_clients'] if not client == ws:
await client.send_str(msg.data)
elif msg.type == aiohttp.WSMsgType.ERROR:
print('ws connection closed with exception %s' % ws.exception())
app['broadcast_clients'].remove(ws)
return ws
app = web.Application()
view = DatasetWebSocketView()
app['broadcast_clients'] = []
app.router.add_get('/db', view.handle)
app.router.add_get('/broadcast', sync_view.handle)
+148 -64
View File
@@ -1,67 +1,151 @@
_C='delete' import functools
_B='set' import json
_A='get'
import functools,json
from snek.system import security from snek.system import security
cache=functools.cache
CACHE_MAX_ITEMS_DEFAULT=5000 cache = functools.cache
CACHE_MAX_ITEMS_DEFAULT = 5000
class Cache: class Cache:
def __init__(A,app,max_items=CACHE_MAX_ITEMS_DEFAULT):A.app=app;A.cache={};A.max_items=max_items;A.stats={};A.lru=[];A.version=15505 def __init__(self, app, max_items=CACHE_MAX_ITEMS_DEFAULT):
async def get(A,args): self.app = app
B=args;await A.update_stat(B,_A) self.cache = {}
try:A.lru.pop(A.lru.index(B)) self.max_items = max_items
except:return self.stats = {}
A.lru.insert(0,B) self.enabled = False
while len(A.lru)>A.max_items:A.cache.pop(A.lru[-1]);A.lru.pop() self.lru = []
return A.cache[B] self.version = ((42 + 420 + 1984 + 1990 + 10 + 6 + 71 + 3004 + 7245) ^ 1337) + 4
async def get_stats(A):
C=[] async def get(self, args):
for B in A.lru:C.append({'key':B,_B:A.stats[B][_B],_A:A.stats[B][_A],_C:A.stats[B][_C],'value':str(A.serialize(A.cache[B].record))}) if not self.enabled:
return C return None
def serialize(C,obj):B=None;A=obj.copy();A.pop('created_at',B);A.pop('deleted_at',B);A.pop('email',B);A.pop('password',B);return A await self.update_stat(args, "get")
async def update_stat(A,key,action): try:
C=action;B=key self.lru.pop(self.lru.index(args))
if B not in A.stats:A.stats[B]={_B:0,_A:0,_C:0} except:
A.stats[B][C]=A.stats[B][C]+1 # print("Cache miss!", args, flush=True)
def json_default(B,value): return None
A=value self.lru.insert(0, args)
try:return json.dumps(A.__dict__,default=str) while len(self.lru) > self.max_items:
except:return str(A) self.cache.pop(self.lru[-1])
async def create_cache_key(A,args,kwargs):return await security.hash(json.dumps({'args':args,'kwargs':kwargs},sort_keys=True,default=A.json_default)) self.lru.pop()
async def set(A,args,result): # print("Cache hit!", args, flush=True)
B=args;C=B not in A.cache;A.cache[B]=result;await A.update_stat(B,_B) return self.cache[args]
try:A.lru.pop(A.lru.index(B))
except(ValueError,IndexError):pass async def get_stats(self):
A.lru.insert(0,B) all_ = []
while len(A.lru)>A.max_items:A.cache.pop(A.lru[-1]);A.lru.pop() for key in self.lru:
if C:A.version+=1 all_.append(
async def delete(A,args): {
B=args;await A.update_stat(B,_C) "key": key,
if B in A.cache: "set": self.stats[key]["set"],
try:A.lru.pop(A.lru.index(B)) "get": self.stats[key]["get"],
except IndexError:pass "delete": self.stats[key]["delete"],
del A.cache[B] "value": str(self.serialize(self.cache[key].record)),
def async_cache(A,func): }
@functools.wraps(func) )
async def B(*B,**C): return all_
D=await A.create_cache_key(B,C);E=await A.get(D)
if E:return E def serialize(self, obj):
F=await func(*B,**C);await A.set(D,F);return F cpy = obj.copy()
return B cpy.pop("created_at", None)
def async_delete_cache(A,func): cpy.pop("deleted_at", None)
@functools.wraps(func) cpy.pop("email", None)
async def B(*C,**D): cpy.pop("password", None)
B=await A.create_cache_key(C,D) return cpy
if B in A.cache:
try:A.lru.pop(A.lru.index(B)) async def update_stat(self, key, action):
except IndexError:pass if key not in self.stats:
del A.cache[B] self.stats[key] = {"set": 0, "get": 0, "delete": 0}
return await func(*C,**D) self.stats[key][action] = self.stats[key][action] + 1
return B
def json_default(self, value):
# if hasattr(value, "to_json"):
# return value.to_json()
try:
return json.dumps(value.__dict__, default=str)
except:
return str(value)
async def create_cache_key(self, args, kwargs):
return await security.hash(
json.dumps(
{"args": args, "kwargs": kwargs},
sort_keys=True,
default=self.json_default,
)
)
async def set(self, args, result):
if not self.enabled:
return
is_new = args not in self.cache
self.cache[args] = result
await self.update_stat(args, "set")
try:
self.lru.pop(self.lru.index(args))
except (ValueError, IndexError):
pass
self.lru.insert(0, args)
while len(self.lru) > self.max_items:
self.cache.pop(self.lru[-1])
self.lru.pop()
if is_new:
self.version += 1
# print(f"Cache store! {len(self.lru)} items. New version:", self.version, flush=True)
async def delete(self, args):
if not self.enabled:
return
await self.update_stat(args, "delete")
if args in self.cache:
try:
self.lru.pop(self.lru.index(args))
except IndexError:
pass
del self.cache[args]
def async_cache(self, func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
cache_key = await self.create_cache_key(args, kwargs)
cached = await self.get(cache_key)
if cached:
return cached
result = await func(*args, **kwargs)
await self.set(cache_key, result)
return result
return wrapper
def async_delete_cache(self, func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
cache_key = await self.create_cache_key(args, kwargs)
if cache_key in self.cache:
try:
self.lru.pop(self.lru.index(cache_key))
except IndexError:
pass
del self.cache[cache_key]
return await func(*args, **kwargs)
return wrapper
def async_cache(func): def async_cache(func):
B={} cache = {}
@functools.wraps(func)
async def A(*A): @functools.wraps(func)
if A in B:return B[A] async def wrapper(*args):
C=await func(*A);B[A]=C;return C if args in cache:
return A return cache[args]
result = await func(*args)
cache[args] = result
return result
return wrapper
+115 -27
View File
@@ -1,32 +1,120 @@
_B='fields' # Written by retoor@molodetz.nl
_A=None
# This code defines a framework for handling HTML elements as Python objects, including specific classes for HTML, form input, and form button elements. It offers methods to convert these elements to JSON, manipulate them, and validate form data.
# This code uses the `snek.system.model` library for managing model fields.
# MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from snek.system import model from snek.system import model
class HTMLElement(model.ModelField): class HTMLElement(model.ModelField):
def __init__(A,id=_A,tag='div',name=_A,html=_A,class_name=_A,text=_A,*B,**C):A.tag=tag;A.text=text;A.id=id;A.class_name=class_name or name;A.html=html;super().__init__(*B,name=name,**C) def __init__(
async def to_json(B):A=await super().to_json();A['text']=B.text;A['id']=B.id;A['html']=B.html;A['class_name']=B.class_name;A['tag']=B.tag;return A self,
class FormElement(HTMLElement):0 id=None,
tag="div",
name=None,
html=None,
class_name=None,
text=None,
*args,
**kwargs,
):
self.tag = tag
self.text = text
self.id = id
self.class_name = class_name or name
self.html = html
super().__init__(name=name, *args, **kwargs)
async def to_json(self):
result = await super().to_json()
result["text"] = self.text
result["id"] = self.id
result["html"] = self.html
result["class_name"] = self.class_name
result["tag"] = self.tag
return result
class FormElement(HTMLElement):
pass
class FormInputElement(FormElement): class FormInputElement(FormElement):
def __init__(A,type='text',place_holder=_A,*B,**C):super().__init__(*B,tag='input',**C);A.place_holder=place_holder;A.type=type def __init__(self, type="text", place_holder=None, *args, **kwargs):
async def to_json(B):A=await super().to_json();A['place_holder']=B.place_holder;A['type']=B.type;return A super().__init__(tag="input", *args, **kwargs)
self.place_holder = place_holder
self.type = type
async def to_json(self):
data = await super().to_json()
data["place_holder"] = self.place_holder
data["type"] = self.type
return data
class FormButtonElement(FormElement): class FormButtonElement(FormElement):
def __init__(C,tag='button',*A,**B):super().__init__(*A,tag=tag,**B) def __init__(self, tag="button", *args, **kwargs):
super().__init__(tag=tag, *args, **kwargs)
class Form(model.BaseModel): class Form(model.BaseModel):
@property @property
def html_elements(self):return[A for A in self.fields if isinstance(A,HTMLElement)] def html_elements(self):
def set_user_data(A,data):return super().set_user_data(data.get(_B)) return [element for element in self.fields if isinstance(element, HTMLElement)]
async def to_json(D,encode=False):
B='is_valid';E=await super().to_json();C={} def set_user_data(self, data):
for A in E.keys(): return super().set_user_data(data.get("fields"))
if A==B:continue
F=getattr(D,A) async def to_json(self, encode=False):
if isinstance(F,HTMLElement): elements = await super().to_json()
try:C[A]=E[A] html_elements = {}
except KeyError:pass for element in elements.keys():
G=all(A[B]for A in C.values());return{_B:C,B:G,'errors':await D.errors} if element == "is_valid":
@property # is_valid is async get property so we can't do getattr on it
async def errors(self): continue
A=[] field = getattr(self, element)
for B in self.html_elements:A+=await B.errors if isinstance(field, HTMLElement):
return A try:
@property html_elements[element] = elements[element]
async def is_valid(self):return False except KeyError:
pass
is_valid = all(field["is_valid"] for field in html_elements.values())
return {
"fields": html_elements,
"is_valid": is_valid,
"errors": await self.errors,
}
@property
async def errors(self):
result = []
for field in self.html_elements:
result += await field.errors
return result
@property
async def is_valid(self):
# This is not good, but timebox to resolve issue exceeded.
return False
+103 -37
View File
@@ -1,44 +1,110 @@
import asyncio,pathlib,uuid,zlib # Written by retoor@molodetz.nl
# This script enables downloading, processing, and caching web content, including taking website screenshots and repairing links in HTML content.
# Imports used: aiohttp, aiohttp.web for creating web servers and handling async requests; app.cache for caching utilities; BeautifulSoup from bs4 for HTML parsing; imgkit for creating screenshots.
# The MIT License (MIT)
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import asyncio
import pathlib
import uuid
import zlib
from urllib.parse import urljoin from urllib.parse import urljoin
import aiohttp,imgkit
import aiohttp
import imgkit
from app.cache import time_cache_async from app.cache import time_cache_async
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
async def crc32(data): async def crc32(data):
A=data try:
try:A=A.encode() data = data.encode()
except:pass except:
return'crc32'+str(zlib.crc32(A)) pass
async def get_file(name,suffix='.cache'): return "crc32" + str(zlib.crc32(data))
A=name;A=await crc32(A);B=pathlib.Path('.').joinpath('cache')
if not B.exists():B.mkdir(parents=True,exist_ok=True)
return B.joinpath(A+suffix) async def get_file(name, suffix=".cache"):
async def public_touch(name=None):A=pathlib.Path('.').joinpath(str(uuid.uuid4())+name);A.open('wb').close();return A name = await crc32(name)
path = pathlib.Path(".").joinpath("cache")
if not path.exists():
path.mkdir(parents=True, exist_ok=True)
return path.joinpath(name + suffix)
async def public_touch(name=None):
path = pathlib.Path(".").joinpath(str(uuid.uuid4()) + name)
path.open("wb").close()
return path
async def create_site_photo(url): async def create_site_photo(url):
A=url;C=asyncio.get_event_loop() loop = asyncio.get_event_loop()
if not A.startswith('https'):A='https://'+A if not url.startswith("https"):
B=await get_file('site-screenshot-'+A,'.png') url = "https://" + url
if B.exists():return B output_path = await get_file("site-screenshot-" + url, ".png")
B.touch()
def D():imgkit.from_url(A,B.absolute());return B if output_path.exists():
return await C.run_in_executor(None,D) return output_path
async def repair_links(base_url,html_content): output_path.touch()
D='http';E=base_url;B='src';C='href';F=BeautifulSoup(html_content,'html.parser')
for A in F.find_all(['a','img','link']): def make_photo():
if A.has_attr(C)and not A[C].startswith(D):A[C]=urljoin(E,A[C]) imgkit.from_url(url, output_path.absolute())
if A.has_attr(B)and not A[B].startswith(D):A[B]=urljoin(E,A[B]) return output_path
return F.prettify()
async def is_html_content(content): return await loop.run_in_executor(None, make_photo)
B=False;A=content
if not A:return B
try:A=A.decode(errors='ignore') async def repair_links(base_url, html_content):
except:pass soup = BeautifulSoup(html_content, "html.parser")
C=['<html','<img','<p','<span','<div'];A=A.lower() for tag in soup.find_all(["a", "img", "link"]):
for D in C: if tag.has_attr("href") and not tag["href"].startswith("http"):
if D in A:return True tag["href"] = urljoin(base_url, tag["href"])
return B if tag.has_attr("src") and not tag["src"].startswith("http"):
tag["src"] = urljoin(base_url, tag["src"])
return soup.prettify()
async def is_html_content(content: bytes):
if not content:
return False
try:
content = content.decode(errors="ignore")
except:
pass
marks = ["<html", "<img", "<p", "<span", "<div"]
content = content.lower()
for mark in marks:
if mark in content:
return True
return False
@time_cache_async(120) @time_cache_async(120)
async def get(url): async def get(url):
async with aiohttp.ClientSession()as B: async with aiohttp.ClientSession() as session:
C=await B.get(url);A=await C.text() response = await session.get(url)
if await is_html_content(A):A=(await repair_links(url,A)).encode() content = await response.text()
return A if await is_html_content(content):
content = (await repair_links(url, content)).encode()
return content
+75 -34
View File
@@ -1,37 +1,78 @@
_A='uid' DEFAULT_LIMIT = 30
DEFAULT_LIMIT=30
import typing import typing
import asyncio
from snek.system.model import BaseModel from snek.system.model import BaseModel
class BaseMapper: class BaseMapper:
model_class:BaseModel=None;default_limit:int=DEFAULT_LIMIT;table_name:str=None
def __init__(A,app):A.app=app;A.default_limit=A.__class__.default_limit model_class: BaseModel = None
@property default_limit: int = DEFAULT_LIMIT
def db(self):return self.app.db table_name: str = None
async def new(A):return A.model_class(mapper=A,app=A.app)
@property def __init__(self, app):
def table(self):return self.db[self.table_name] self.app = app
async def get(B,uid=None,**C):
if uid:C[_A]=uid self.default_limit = self.__class__.default_limit
A=B.table.find_one(**C)
if not A:return @property
A=dict(A);D=await B.new() def db(self):
for(E,F)in A.items():D[E]=F return self.app.db
return D;return await B.model_class.from_record(mapper=B,record=A)
async def exists(A,**B):return A.table.exists(**B) @property
async def count(A,**B):return A.table.count(**B) def loop(self):
async def save(B,model): return asyncio.get_event_loop()
A=model
if not A.record.get(_A):raise Exception(f"Attempt to save without uid: {A.record}.") async def run_in_executor(self, func, *args, **kwargs):
A.updated_at.update();return B.table.upsert(A.record,[_A]) return await self.loop.run_in_executor(None, lambda: func(*args, **kwargs))
async def find(A,**B):
C='_limit' async def new(self):
if not B.get(C):B[C]=A.default_limit return self.model_class(mapper=self, app=self.app)
for E in A.table.find(**B):
D=await A.new() @property
for(F,G)in E.items():D[F]=G def table(self):
yield D return self.db[self.table_name]
async def query(A,sql,*B):
for C in A.db.query(sql,*B):yield dict(C) async def get(self, uid: str = None, **kwargs) -> BaseModel:
async def delete(B,**A): if uid:
if not A or not isinstance(A,dict):raise Exception("Can't execute delete with no filter.") kwargs["uid"] = uid
return B.table.delete(**A)
record = await self.run_in_executor(self.table.find_one,**kwargs)
if not record:
return None
record = dict(record)
model = await self.new()
for key, value in record.items():
model[key] = value
return model
return await self.model_class.from_record(mapper=self, record=record)
async def exists(self, **kwargs):
return await self.run_in_executor(self.table.exists,**kwargs)
async def count(self, **kwargs) -> int:
return await self.run_in_executor(self.table.count, **kwargs)
async def save(self, model: BaseModel) -> bool:
if not model.record.get("uid"):
raise Exception(f"Attempt to save without uid: {model.record}.")
model.updated_at.update()
return await self.run_in_executor(self.table.upsert, model.record, ["uid"])
async def find(self, **kwargs) -> typing.AsyncGenerator:
if not kwargs.get("_limit"):
kwargs["_limit"] = self.default_limit
for record in await self.run_in_executor(self.table.find, **kwargs):
model = await self.new()
for key, value in record.items():
model[key] = value
yield model
async def query(self, sql, *args):
for record in await self.run_in_executor(self.db.query,sql, *args):
yield dict(record)
async def delete(self, **kwargs) -> int:
if not kwargs or not isinstance(kwargs, dict):
raise Exception("Can't execute delete with no filter.")
return await self.run_in_executor(self.table.delete, **kwargs)
+90 -25
View File
@@ -1,35 +1,100 @@
_A=True # Original source: https://brandonjay.dev/posts/2021/render-markdown-html-in-python-with-jinja2
from types import SimpleNamespace from types import SimpleNamespace
from app.cache import time_cache_async from app.cache import time_cache_async
from mistune import HTMLRenderer,Markdown from mistune import HTMLRenderer, Markdown
from mistune.plugins.formatting import strikethrough
from mistune.plugins.spoiler import spoiler
from mistune.plugins.url import url
from pygments import highlight from pygments import highlight
from pygments.formatters import html from pygments.formatters import html
from pygments.lexers import get_lexer_by_name from pygments.lexers import get_lexer_by_name
class MarkdownRenderer(HTMLRenderer): class MarkdownRenderer(HTMLRenderer):
_allow_harmful_protocols=_A
def __init__(A,app,template):A.template=template;A.app=app;A.env=A.app.jinja2_env;B=html.HtmlFormatter();A.env.globals['highlight_styles']=B.get_style_defs() _allow_harmful_protocols = True
def _escape(A,str):return str
def get_lexer(A,lang,default='bash'): def __init__(self, app, template):
try:return get_lexer_by_name(lang,stripall=_A) super().__init__(False, True)
except:return get_lexer_by_name(default,stripall=_A)
def block_code(B,code,lang=None,info=None): self.template = template
A=lang
if not A:A=info self.app = app
if not A:A='bash' self.env = self.app.jinja2_env
C=B.get_lexer(A);D=html.HtmlFormatter(lineseparator='<br>');E=highlight(code,C,D);return E formatter = html.HtmlFormatter()
def render(A):B=A.app.template_path.joinpath(A.template).read_text();C=MarkdownRenderer(A.app,A.template);D=Markdown(renderer=C);return D(B) self.env.globals["highlight_styles"] = formatter.get_style_defs()
def render_markdown_sync(app,markdown_string):A=MarkdownRenderer(app,None);B=Markdown(renderer=A);return B(markdown_string)
def _escape(self, str):
return str ##escape(str)
def get_lexer(self, lang, default="bash"):
try:
return get_lexer_by_name(lang, stripall=True)
except:
return get_lexer_by_name(default, stripall=True)
def block_code(self, code, lang=None, info=None):
if not lang:
lang = info
if not lang:
lang = "bash"
lexer = self.get_lexer(lang)
formatter = html.HtmlFormatter(lineseparator="<br>")
result = highlight(code, lexer, formatter)
return result
def render(self):
markdown_string = self.app.template_path.joinpath(self.template).read_text()
renderer = MarkdownRenderer(self.app, self.template)
markdown = Markdown(renderer=renderer)
return markdown(markdown_string)
# def image(self, text: str, url: str, title: Optional[str] = None) -> str:
# src = self.safe_url(url)
# alt = escape(striptags(text))
# s = '<picture><srcset srcset="' + src +'" /><img src="' + src + '" alt="' + alt + '"'
# if title:
# s += ' title="' + safe_entity(title) + '"'
# return s + ' /></picture>'
def render_markdown_sync(app, markdown_string):
renderer = MarkdownRenderer(app, None)
markdown = Markdown(renderer=renderer, plugins=[url, strikethrough, spoiler])
return markdown(markdown_string)
@time_cache_async(120) @time_cache_async(120)
async def render_markdown(app,markdown_string):return render_markdown_sync(app,markdown_string) async def render_markdown(app, markdown_string):
from jinja2 import TemplateSyntaxError,nodes return render_markdown_sync(app, markdown_string)
from jinja2 import TemplateSyntaxError, nodes
from jinja2.ext import Extension from jinja2.ext import Extension
from jinja2.nodes import Const from jinja2.nodes import Const
# Source: https://ron.sh/how-to-write-a-jinja2-extension/
class MarkdownExtension(Extension): class MarkdownExtension(Extension):
tags={'markdown'} tags = {"markdown"}
def __init__(A,environment):B=environment;A.app=SimpleNamespace(jinja2_env=B);super(MarkdownExtension,A).__init__(B)
def parse(D,parser): def __init__(self, environment):
A=parser;E=next(A.stream).lineno;B=[Const('')];C='' self.app = SimpleNamespace(jinja2_env=environment)
try:B=[A.parse_expression()] super(MarkdownExtension, self).__init__(environment)
except TemplateSyntaxError:C=A.parse_statements(['name:endmarkdown'],drop_needle=_A)
return nodes.CallBlock(D.call_method('_to_html',B),[],[],C).set_lineno(E) def parse(self, parser):
def _to_html(A,md_file,caller):return render_markdown_sync(A.app,caller()) line_number = next(parser.stream).lineno
md_file = [Const("")]
body = ""
try:
md_file = [parser.parse_expression()]
except TemplateSyntaxError:
body = parser.parse_statements(["name:endmarkdown"], drop_needle=True)
return nodes.CallBlock(
self.call_method("_to_html", md_file), [], [], body
).set_lineno(line_number)
def _to_html(self, md_file, caller):
return render_markdown_sync(self.app, caller())
+48 -16
View File
@@ -1,21 +1,53 @@
_D='Access-Control-Allow-Credentials' # Written by retoor@molodetz.nl
_C='Access-Control-Allow-Headers'
_B='Access-Control-Allow-Methods' # This code provides middleware functions for an aiohttp server to manage and modify CORS (Cross-Origin Resource Sharing) headers.
_A='Access-Control-Allow-Origin'
# Imports from 'aiohttp' library are used to create middleware; they are not part of Python's standard library.
# MIT License: This code is distributed under the MIT License.
from aiohttp import web from aiohttp import web
@web.middleware @web.middleware
async def no_cors_middleware(request,handler):A=await handler(request);A.headers.pop(_A,None);return A async def no_cors_middleware(request, handler):
response = await handler(request)
response.headers.pop("Access-Control-Allow-Origin", None)
return response
@web.middleware @web.middleware
async def cors_allow_middleware(request,handler):A=await handler(request);A.headers[_A]='*';A.headers[_B]='GET, POST, OPTIONS, PUT, DELETE, MOVE, COPY, HEAD, LOCK, UNLOCK, PATCH, PROPFIND';A.headers[_C]='*';A.headers[_D]='true';return A async def cors_allow_middleware(request, handler):
response = await handler(request)
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = (
"GET, POST, OPTIONS, PUT, DELETE, MOVE, COPY, HEAD, LOCK, UNLOCK, PATCH, PROPFIND"
)
response.headers["Access-Control-Allow-Headers"] = "*"
response.headers["Access-Control-Allow-Credentials"] = "true"
return response
@web.middleware @web.middleware
async def auth_middleware(request,handler): async def auth_middleware(request, handler):
B='uid';C='user';A=request;A[C]=None request["user"] = None
if A.session.get(B)and A.session.get('logged_in'):A[C]=await A.app.services.user.get(uid=A.app.session.get(B)) if request.session.get("uid") and request.session.get("logged_in"):
return await handler(A) request["user"] = await request.app.services.user.get(
uid=request.app.session.get("uid")
)
return await handler(request)
@web.middleware @web.middleware
async def cors_middleware(request,handler): async def cors_middleware(request, handler):
C='Allow';D=handler;B=request if request.headers.get("Allow"):
if B.headers.get(C):return await D(B) return await handler(request)
A=await D(B)
if B.headers.get(C):return A response = await handler(request)
A.headers[_A]='*';A.headers[_B]='GET, POST, PUT, DELETE, OPTIONS';A.headers[_C]='*';A.headers[_D]='true';return A if request.headers.get("Allow"):
return response
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "*"
response.headers["Access-Control-Allow-Credentials"] = "true"
return response
+367 -129
View File
@@ -1,139 +1,377 @@
_I='deleted_at' # Written by retoor@molodetz.nl
_H='updated_at'
_G='created_at' # The script defines a flexible validation and field management system for models, with capabilities for setting attributes, validation, error handling, and JSON conversion. It includes classes for managing various field types with specific properties such as UUID, timestamps for creation and updates, and custom validation rules.
_F='is_valid'
_E='name' # This script utilizes external Python libraries such as 're' for regex operations, 'uuid' for generating unique identifiers, and 'json' for data interchange. The 'datetime' and 'timezone' modules from the Python standard library are used for date and time operations. 'OrderedDict' from 'collections' provides enhanced dictionary capabilities, and 'copy' allows deep copying of objects.
_D=False
_C='value' # MIT License
_B=True #
_A=None # Permission is hereby granted, free of charge, to any person obtaining a copy
import copy,json,re,uuid # of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import copy
import json
import re
import uuid
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime,timezone from datetime import datetime, timezone
TIMESTAMP_REGEX='^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}\\+\\d{2}:\\d{2}$'
def now():return str(datetime.now(timezone.utc)) TIMESTAMP_REGEX = r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}\+\d{2}:\d{2}$"
def add_attrs(**A):
def B(func):
for(B,C)in A.items():setattr(func,B,C) def now():
return func return str(datetime.now(timezone.utc))
return B
def validate_attrs(required=_D,min_length=_A,max_length=_A,regex=_A,**A):
def B(func):return add_attrs(required=required,min_length=min_length,max_length=max_length,regex=regex,**A)(func) def add_attrs(**kwargs):
def decorator(func):
for key, value in kwargs.items():
setattr(func, key, value)
return func
return decorator
def validate_attrs(
required=False, min_length=None, max_length=None, regex=None, **kwargs
):
def decorator(func):
return add_attrs(
required=required,
min_length=min_length,
max_length=max_length,
regex=regex,
**kwargs,
)(func)
class Validator: class Validator:
_index=0 _index = 0
@property
def value(self):return self._value @property
@value.setter def value(self):
def value(self,val):self._value=json.loads(json.dumps(val,default=str)) return self._value
@property
def initial_value(self):return self.value @value.setter
def custom_validation(A):return _B def value(self, val):
def __init__(A,required=_D,min_num=_A,max_num=_A,min_length=_A,max_length=_A,regex=_A,value=_A,kind=_A,help_text=_A,app=_A,model=_A,**B):A.index=Validator._index;Validator._index+=1;A.app=app;A.model=model;A.required=required;A.min_num=min_num;A.max_num=max_num;A.min_length=min_length;A.max_length=max_length;A.regex=regex;A._value=_A;A.value=value;A.kind=kind;A.help_text=help_text;A.__dict__.update(B) self._value = json.loads(json.dumps(val, default=str))
@property
async def errors(self): @property
A=self;B=[] def initial_value(self):
if A.value is _A and A.required:B.append('Field is required.');return B return self.value
if A.value is _A:return B
if A.kind in[int,float]: def custom_validation(self):
if A.min_num is not _A and A.value<A.min_num:B.append(f"Field should be minimal {A.min_num}.") return True
if A.max_num is not _A and A.value>A.max_num:B.append(f"Field should be maximal {A.max_num}.")
if A.min_length is not _A and len(A.value)<A.min_length:B.append(f"Field should be minimal {A.min_length} characters long.") def __init__(
if A.max_length is not _A and len(A.value)>A.max_length:B.append(f"Field should be maximal {A.max_length} characters long.") self,
if A.regex and A.value and not re.match(A.regex,A.value):B.append('Invalid value.') required=False,
if A.kind and not isinstance(A.value,A.kind):B.append(f"Invalid kind. It is supposed to be {A.kind}.") min_num=None,
return B max_num=None,
async def validate(B): min_length=None,
A=await B.errors max_length=None,
if A:raise ValueError(f"Errors: {A}.") regex=None,
return _B value=None,
def __repr__(A):return str(A.to_json()) kind=None,
@property help_text=None,
async def is_valid(self): app=None,
try:await self.validate();return _B model=None,
except ValueError:return _D **kwargs,
async def to_json(A):B=await A.errors;C=await A.is_valid;return{'required':A.required,'min_num':A.min_num,'max_num':A.max_num,'min_length':A.min_length,'max_length':A.max_length,'regex':A.regex,_C:A.value,'kind':str(A.kind),'help_text':A.help_text,'errors':B,_F:C,'index':A.index} ):
self.index = Validator._index
Validator._index += 1
self.app = app
self.model = model
self.required = required
self.min_num = min_num
self.max_num = max_num
self.min_length = min_length
self.max_length = max_length
self.regex = regex
self._value = None
self.value = value
self.kind = kind
self.help_text = help_text
self.__dict__.update(kwargs)
@property
async def errors(self):
error_list = []
if self.value is None and self.required:
error_list.append("Field is required.")
return error_list
if self.value is None:
return error_list
if self.kind in [int, float]:
if self.min_num is not None and self.value < self.min_num:
error_list.append(f"Field should be minimal {self.min_num}.")
if self.max_num is not None and self.value > self.max_num:
error_list.append(f"Field should be maximal {self.max_num}.")
if self.min_length is not None and len(self.value) < self.min_length:
error_list.append(
f"Field should be minimal {self.min_length} characters long."
)
if self.max_length is not None and len(self.value) > self.max_length:
error_list.append(
f"Field should be maximal {self.max_length} characters long."
)
if self.regex and self.value and not re.match(self.regex, self.value):
error_list.append("Invalid value.")
if self.kind and not isinstance(self.value, self.kind):
error_list.append(f"Invalid kind. It is supposed to be {self.kind}.")
return error_list
async def validate(self):
errors = await self.errors
if errors:
raise ValueError(f"Errors: {errors}.")
return True
def __repr__(self):
return str(self.to_json())
@property
async def is_valid(self):
try:
await self.validate()
return True
except ValueError:
return False
async def to_json(self):
errors = await self.errors
is_valid = await self.is_valid
return {
"required": self.required,
"min_num": self.min_num,
"max_num": self.max_num,
"min_length": self.min_length,
"max_length": self.max_length,
"regex": self.regex,
"value": self.value,
"kind": str(self.kind),
"help_text": self.help_text,
"errors": errors,
"is_valid": is_valid,
"index": self.index,
}
class ModelField(Validator): class ModelField(Validator):
index=1
def __init__(A,name=_A,save=_B,*B,**C):A.name=name;A.save=save;super().__init__(*B,**C) index = 1
async def to_json(B):A=await super().to_json();A[_E]=B.name;return A
def __init__(self, name=None, save=True, *args, **kwargs):
self.name = name
self.save = save
super().__init__(*args, **kwargs)
async def to_json(self):
result = await super().to_json()
result["name"] = self.name
return result
class CreatedField(ModelField): class CreatedField(ModelField):
@property
def initial_value(self):return now() @property
def update(A): def initial_value(self):
if not A.value:A.value=now() return now()
def update(self):
if not self.value:
self.value = now()
class UpdatedField(ModelField): class UpdatedField(ModelField):
def update(A):A.value=now()
def update(self):
self.value = now()
class DeletedField(ModelField): class DeletedField(ModelField):
def update(A):A.value=now()
def update(self):
self.value = now()
class UUIDField(ModelField): class UUIDField(ModelField):
@property
def value(self):return str(self._value) @property
@value.setter def value(self):
def value(self,val):self._value=str(val) return str(self._value)
@property
def initial_value(self):return str(uuid.uuid4()) @value.setter
def value(self, val):
self._value = str(val)
@property
def initial_value(self):
return str(uuid.uuid4())
class BaseModel: class BaseModel:
uid=UUIDField(name='uid',required=_B);created_at=CreatedField(name=_G,required=_B,regex=TIMESTAMP_REGEX,place_holder='Created at');updated_at=UpdatedField(name=_H,regex=TIMESTAMP_REGEX,place_holder='Updated at');deleted_at=DeletedField(name=_I,regex=TIMESTAMP_REGEX,place_holder='Deleted at')
@classmethod uid = UUIDField(name="uid", required=True)
async def from_record(B,record,mapper):A=B();A.mapper=mapper;A.record=record;return A created_at = CreatedField(
@property name="created_at",
def mapper(self):return self._mapper required=True,
@mapper.setter regex=TIMESTAMP_REGEX,
def mapper(self,value):self._mapper=value place_holder="Created at",
@property )
def record(self):return{A:B.value for(A,B)in self.fields.items()} updated_at = UpdatedField(
@record.setter name="updated_at", regex=TIMESTAMP_REGEX, place_holder="Updated at"
def record(self,val): )
A=self deleted_at = DeletedField(
for(B,C)in val.items(): name="deleted_at", regex=TIMESTAMP_REGEX, place_holder="Deleted at"
D=A.fields.get(B) )
if not D:continue
A[B]=C @classmethod
return A async def from_record(cls, record, mapper):
def __init__(A,*F,**C): model = cls()
D='app';A._mapper=C.get('mapper');A.app=C.get(D);A.fields={} model.mapper = mapper
for B in dir(A.__class__): model.record = record
E=getattr(A.__class__,B) return model
if isinstance(E,Validator):A.__dict__[B]=copy.deepcopy(E);A.__dict__[B].value=C.pop(B,A.__dict__[B].initial_value);A.fields[B]=A.__dict__[B];A.fields[B].model=A;A.fields[B].app=C.get(D)
def __setitem__(B,key,value): @property
A=B.__dict__.get(key) def mapper(self):
if isinstance(A,Validator):A.value=value return self._mapper
def __getattr__(B,key):
A=B.__dict__.get(key) @mapper.setter
if isinstance(A,Validator):return A.value def mapper(self, value):
return A self._mapper = value
def set_user_data(C,data):
for(D,A)in data.items(): @property
B=C.fields.get(D) def record(self):
if not B:continue return {key: field.value for key, field in self.fields.items()}
if A.get(_E):A=A.get(_C)
B.value=A @record.setter
@property def record(self, val):
async def is_valid(self):return all([await A.is_valid for A in self.fields.values()]) for key, value in val.items():
def __getitem__(B,key): field = self.fields.get(key)
A=B.__dict__.get(key) if not field:
if isinstance(A,Validator):return A.value continue
def __setattr__(A,key,value): self[key] = value
B=value;C=getattr(A,key) return self
if isinstance(C,Validator):C.value=B
else:A.__dict__[key]=B def __init__(self, *args, **kwargs):
@property self._mapper = kwargs.get("mapper")
async def recordz(self): self.app = kwargs.get("app")
D=await self.to_json();B={} self.fields = {}
for(C,A)in D.items(): for key in dir(self.__class__):
if not isinstance(A,dict)or _C not in A:continue obj = getattr(self.__class__, key)
if getattr(self,C).save:B[C]=A.get(_C)
return B if isinstance(obj, Validator):
async def to_json(A,encode=_D): self.__dict__[key] = copy.deepcopy(obj)
B=OrderedDict({'uid':A.uid.value,_G:A.created_at.value,_H:A.updated_at.value,_I:A.deleted_at.value,_F:await A.is_valid}) self.__dict__[key].value = kwargs.pop(
for(C,D)in A.fields.items(): key, self.__dict__[key].initial_value
if C=='record':continue )
D=A.__dict__[C] self.fields[key] = self.__dict__[key]
if hasattr(D,_C):B[C]=await D.to_json() self.fields[key].model = self
if encode:return json.dumps(B,indent=2) self.fields[key].app = kwargs.get("app")
return B
def __setitem__(self, key, value):
obj = self.__dict__.get(key)
if isinstance(obj, Validator):
obj.value = value
def __getattr__(self, key):
obj = self.__dict__.get(key)
if isinstance(obj, Validator):
return obj.value
return obj
def set_user_data(self, data):
for key, value in data.items():
field = self.fields.get(key)
if not field:
continue
if value.get("name"):
value = value.get("value")
field.value = value
@property
async def is_valid(self):
return all([await field.is_valid for field in self.fields.values()])
def __getitem__(self, key):
obj = self.__dict__.get(key)
if isinstance(obj, Validator):
return obj.value
def __setattr__(self, key, value):
obj = getattr(self, key)
if isinstance(obj, Validator):
obj.value = value
else:
self.__dict__[key] = value
@property
async def recordz(self):
obj = await self.to_json()
record = {}
for key, value in obj.items():
if not isinstance(value, dict) or "value" not in value:
continue
if getattr(self, key).save:
record[key] = value.get("value")
return record
async def to_json(self, encode=False):
model_data = OrderedDict(
{
"uid": self.uid.value,
"created_at": self.created_at.value,
"updated_at": self.updated_at.value,
"deleted_at": self.deleted_at.value,
"is_valid": await self.is_valid,
}
)
for key, value in self.fields.items():
if key == "record":
continue
value = self.__dict__[key]
if hasattr(value, "value"):
model_data[key] = await value.to_json()
if encode:
return json.dumps(model_data, indent=2)
return model_data
class FormElement(ModelField): class FormElement(ModelField):
def __init__(A,place_holder=_A,*B,**C):super().__init__(*B,**C);A.place_holder=place_holder
def __init__(self, place_holder=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.place_holder = place_holder
class FormElement(ModelField): class FormElement(ModelField):
def __init__(A,place_holder=_A,*B,**C):A.place_holder=place_holder;super().__init__(*B,**C)
async def to_json(B):A=await super().to_json();A[_E]=B.name;A['place_holder']=B.place_holder;return A def __init__(self, place_holder=None, *args, **kwargs):
self.place_holder = place_holder
super().__init__(*args, **kwargs)
async def to_json(self):
data = await super().to_json()
data["name"] = self.name
data["place_holder"] = self.place_holder
return data
+12 -6
View File
@@ -1,7 +1,13 @@
class Object: class Object:
def __init__(A,*C,**D):
for B in C: def __init__(self, *args, **kwargs):
if isinstance(B,dict):A.__dict__.update(B) for arg in args:
A.__dict__.update(D) if isinstance(arg, dict):
def __getitem__(A,key):return A.__dict__[key] self.__dict__.update(arg)
def __setitem__(A,key,value):A.__dict__[key]=value self.__dict__.update(kwargs)
def __getitem__(self, key):
return self.__dict__[key]
def __setitem__(self, key, value):
self.__dict__[key] = value
+42 -13
View File
@@ -1,17 +1,46 @@
import cProfile,pstats,sys import cProfile
import pstats
import sys
from aiohttp import web from aiohttp import web
profiler=None
profiler = None
import io import io
@web.middleware @web.middleware
async def profile_middleware(request,handler): async def profile_middleware(request, handler):
global profiler global profiler
if not profiler:profiler=cProfile.Profile() if not profiler:
profiler.enable();B=await handler(request);profiler.disable();A=pstats.Stats(profiler,stream=sys.stdout);A.sort_stats('cumulative');A.print_stats();return B profiler = cProfile.Profile()
async def profiler_handler(request):A=io.StringIO();B=pstats.Stats(profiler,stream=A);C=request.query.get('sort','tot. percall');B.sort_stats(C);B.print_stats();return web.Response(text=A.getvalue()) profiler.enable()
response = await handler(request)
profiler.disable()
stats = pstats.Stats(profiler, stream=sys.stdout)
stats.sort_stats("cumulative")
stats.print_stats()
return response
async def profiler_handler(request):
output = io.StringIO()
stats = pstats.Stats(profiler, stream=output)
sort_by = request.query.get("sort", "tot. percall")
stats.sort_stats(sort_by)
stats.print_stats()
return web.Response(text=output.getvalue())
class Profiler: class Profiler:
def __init__(A):
global profiler def __init__(self):
if profiler is None:profiler=cProfile.Profile() global profiler
A.profiler=profiler if profiler is None:
async def __aenter__(A):A.profiler.enable() profiler = cProfile.Profile()
async def __aexit__(A,*B,**C):A.profiler.disable() self.profiler = profiler
async def __aenter__(self):
self.profiler.enable()
async def __aexit__(self, *args, **kwargs):
self.profiler.disable()
+81 -23
View File
@@ -1,24 +1,82 @@
_A='snekker-de-snek-' import hashlib
import hashlib,uuid import uuid
DEFAULT_SALT=_A
DEFAULT_NS=_A DEFAULT_SALT = "snekker-de-snek-"
DEFAULT_NS = "snekker-de-snek-"
class UIDNS: class UIDNS:
def __init__(A,name):'Initialize UIDNS with a name.';A.name=name def __init__(self, name: str) -> None:
@property """Initialize UIDNS with a name."""
def bytes(self):'Return the bytes representation of the name.';return self.name.encode() self.name = name
def uid(value=None,ns=DEFAULT_NS):
'Generate a UUID based on the provided value and namespace.\n\n Args:\n value (str): The value to generate the UUID from. If None, a new UUID is created.\n ns (str): The namespace to use for UUID generation.\n\n Returns:\n str: The generated UUID as a string.\n ';A=value @property
try:ns=ns.decode() def bytes(self) -> bytes:
except AttributeError:pass """Return the bytes representation of the name."""
if not A:A=str(uuid.uuid4()) return self.name.encode()
try:A=A.decode()
except AttributeError:pass
return str(uuid.uuid5(UIDNS(ns),A)) def uid(value: str = None, ns: str = DEFAULT_NS) -> str:
async def hash(data,salt=DEFAULT_SALT): """Generate a UUID based on the provided value and namespace.
'Hash the given data with the specified salt using SHA-256.\n\n Args:\n data (str): The data to hash.\n salt (str): The salt to use for hashing.\n\n Returns:\n str: The hexadecimal representation of the hashed data.\n ';C='ignore';A=salt;B=data
try:B=B.encode(errors=C) Args:
except AttributeError:pass value (str): The value to generate the UUID from. If None, a new UUID is created.
try:A=A.encode(errors=C) ns (str): The namespace to use for UUID generation.
except AttributeError:pass
D=A+B;E=hashlib.sha256(D);return E.hexdigest() Returns:
async def verify(string,hashed):'Verify if the given string matches the hashed value.\n\n Args:\n string (str): The string to verify.\n hashed (str): The hashed value to compare against.\n\n Returns:\n bool: True if the string matches the hashed value, False otherwise.\n ';return await hash(string)==hashed str: The generated UUID as a string.
"""
try:
ns = ns.decode()
except AttributeError:
pass
if not value:
value = str(uuid.uuid4())
try:
value = value.decode()
except AttributeError:
pass
return str(uuid.uuid5(UIDNS(ns), value))
def hash_sync(data: str, salt: str = DEFAULT_SALT) -> str:
"""Hash the given data with the specified salt using SHA-256.
Args:
data (str): The data to hash.
salt (str): The salt to use for hashing.
Returns:
str: The hexadecimal representation of the hashed data.
"""
try:
data = data.encode(errors="ignore")
except AttributeError:
pass
try:
salt = salt.encode(errors="ignore")
except AttributeError:
pass
salted = salt + data
obj = hashlib.sha256(salted)
return obj.hexdigest()
async def hash(data: str, salt: str = DEFAULT_SALT) -> str:
return hash_sync(data, salt)
def verify_sync(string: str, hashed: str) -> bool:
"""Verify if the given string matches the hashed value.
Args:
string (str): The string to verify.
hashed (str): The hashed value to compare against.
Returns:
bool: True if the string matches the hashed value, False otherwise.
"""
return hash_sync(string) == hashed
async def verify(string: str, hashed: str) -> bool:
return verify_sync(string, hashed)
+63 -38
View File
@@ -1,42 +1,67 @@
_B='uid'
_A=None
from snek.mapper import get_mapper from snek.mapper import get_mapper
from snek.model.user import UserModel from snek.model.user import UserModel
from snek.system.mapper import BaseMapper from snek.system.mapper import BaseMapper
class BaseService: class BaseService:
mapper_name:BaseMapper=_A
@property mapper_name: BaseMapper = None
def services(self):return self.app.services
def __init__(A,app): @property
A.app=app;A.cache=app.cache def services(self):
if A.mapper_name:A.mapper=get_mapper(A.mapper_name,app=A.app) return self.app.services
else:A.mapper=_A
async def exists(C,uid=_A,**A): def __init__(self, app):
B=uid self.app = app
if B: self.cache = app.cache
if not A and await C.cache.get(B):return True if self.mapper_name:
A[_B]=B self.mapper = get_mapper(self.mapper_name, app=self.app)
return await C.count(**A)>0 else:
async def count(A,**B):return await A.mapper.count(**B) self.mapper = None
async def new(A,**B):return await A.mapper.new()
async def query(A,sql,*B): async def exists(self, uid=None, **kwargs):
for C in A.app.db.query(sql,*B):yield C if uid:
async def get(B,uid=_A,**C): if not kwargs and await self.cache.get(uid):
D=uid return True
if D: kwargs["uid"] = uid
if not C: return await self.count(**kwargs) > 0
A=await B.cache.get(D)
if False and A and A.__class__==B.mapper.model_class:return A async def count(self, **kwargs):
C[_B]=D return await self.mapper.count(**kwargs)
A=await B.mapper.get(**C)
if A:await B.cache.set(A[_B],A) async def new(self, **kwargs):
return A return await self.mapper.new()
async def save(B,model):
A=model async def query(self, sql, *args):
if await B.mapper.save(A):await B.cache.set(A[_B],A);return True for record in self.app.db.query(sql, *args):
C=await A.errors;raise Exception(f"Couldn't save model. Errors: f{C}") yield record
async def find(C,**A):
B='_limit' async def get(self, uid=None, **kwargs):
if B not in A or int(A.get(B))>30:A[B]=60 if uid:
async for D in C.mapper.find(**A):yield D if not kwargs:
async def delete(A,**B):return await A.mapper.delete(**B) result = await self.cache.get(uid)
if False and result and result.__class__ == self.mapper.model_class:
return result
kwargs["uid"] = uid
result = await self.mapper.get(**kwargs)
if result:
await self.cache.set(result["uid"], result)
return result
async def save(self, model: UserModel):
# if model.is_valid: You Know why not
if await self.mapper.save(model):
await self.cache.set(model["uid"], model)
return True
errors = await model.errors
raise Exception(f"Couldn't save model. Errors: f{errors}")
async def find(self, **kwargs):
if "_limit" not in kwargs or int(kwargs.get("_limit")) > 30:
kwargs["_limit"] = 60
async for model in self.mapper.find(**kwargs):
yield model
async def delete(self, **kwargs):
return await self.mapper.delete(**kwargs)
File diff suppressed because one or more lines are too long
+111 -47
View File
@@ -1,49 +1,113 @@
_A=None import asyncio
import asyncio,os import os
try:import pty
except Exception as ex:print('You are not able to run a terminal. See error:');print(ex) try:
import pty
except Exception as ex:
print("You are not able to run a terminal. See error:")
print(ex)
import subprocess import subprocess
commands={'alpine':'docker run -it alpine /bin/sh','r':'docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh'}
commands = {
"alpine": "docker run -it alpine /bin/sh",
"r": "docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh",
}
class TerminalSession: class TerminalSession:
def __init__(A,command):A.master,A.slave=_A,_A;A.process=_A;A.sockets=[];A.history=b'';A.history_size=20480;A.command=command;A.start_process(A.command) def __init__(self, command):
def start_process(A,command): self.master, self.slave = None, None
if not A.is_running(): self.process = None
if A.master:os.close(A.master);os.close(A.slave);A.master=_A;A.slave=_A self.sockets = []
A.master,A.slave=pty.openpty();A.process=subprocess.Popen(command.split(' '),stdin=A.slave,stdout=A.slave,stderr=A.slave,bufsize=0,universal_newlines=True) self.history = b""
def is_running(A): self.history_size = 1024 * 20
if not A.process:return False self.command = command
asyncio.get_event_loop();return A.process.poll()is _A self.start_process(self.command)
async def add_websocket(A,ws):A.start_process(A.command);asyncio.create_task(A.read_output(ws))
async def read_output(A,ws): def start_process(self, command):
B=ws;A.sockets.append(B) if not self.is_running():
if len(A.sockets)>1 and A.history: if self.master:
D=0 os.close(self.master)
try:D=A.history.index(b'\n') os.close(self.slave)
except ValueError:pass self.master = None
await B.send_bytes(A.history[D:]);return self.slave = None
E=asyncio.get_event_loop()
while True: self.master, self.slave = pty.openpty()
try: self.process = subprocess.Popen(
C=await E.run_in_executor(_A,os.read,A.master,1024) command.split(" "),
if not C:break stdin=self.slave,
A.history+=C stdout=self.slave,
if len(A.history)>A.history_size:A.history=A.history[:0-A.history_size] stderr=self.slave,
try: bufsize=0,
for B in A.sockets:await B.send_bytes(C) universal_newlines=True,
except:A.sockets.remove(B) )
except Exception:await A.close();break
async def close(A): def is_running(self):
print('Terminating process') if not self.process:
if A.process:A.process.terminate();A.process=_A return False
if A.master:os.close(A.master);os.close(A.slave);A.master=_A;A.slave=_A asyncio.get_event_loop()
print('Terminated process') return self.process.poll() is None
for B in A.sockets:
try:await B.close() async def add_websocket(self, ws):
except Exception:pass self.start_process(self.command)
A.sockets=[] asyncio.create_task(self.read_output(ws))
async def write_input(B,data):
A=data async def read_output(self, ws):
try:A=A.encode() self.sockets.append(ws)
except AttributeError:pass if len(self.sockets) > 1 and self.history:
try:await asyncio.get_event_loop().run_in_executor(_A,os.write,B.master,A) start = 0
except Exception as C:print(C);await B.close() try:
start = self.history.index(b"\n")
except ValueError:
pass
await ws.send_bytes(self.history[start:])
return
loop = asyncio.get_event_loop()
while True:
try:
data = await loop.run_in_executor(None, os.read, self.master, 1024)
if not data:
break
self.history += data
if len(self.history) > self.history_size:
self.history = self.history[: 0 - self.history_size]
try:
for ws in self.sockets:
await ws.send_bytes(data) # Send raw bytes for ANSI support
except:
self.sockets.remove(ws)
except Exception:
await self.close()
break
async def close(self):
print("Terminating process")
if self.process:
self.process.terminate()
self.process = None
if self.master:
os.close(self.master)
os.close(self.slave)
self.master = None
self.slave = None
print("Terminated process")
for ws in self.sockets:
try:
await ws.close()
except Exception:
pass
self.sockets = []
async def write_input(self, data):
try:
data = data.encode()
except AttributeError:
pass
try:
await asyncio.get_event_loop().run_in_executor(
None, os.write, self.master, data
)
except Exception as ex:
print(ex)
await self.close()
+71 -27
View File
@@ -1,31 +1,75 @@
from aiohttp import web from aiohttp import web
from snek.system.markdown import render_markdown from snek.system.markdown import render_markdown
class BaseView(web.View): class BaseView(web.View):
login_required=False
async def _iter(A): login_required = False
if A.login_required and(not A.session.get('logged_in')or not A.session.get('uid')):return web.HTTPFound('/')
return await super()._iter() async def _iter(self):
@property if self.login_required and (
def base_url(self):return str(self.request.url.with_path('').with_query('')) not self.session.get("logged_in") or not self.session.get("uid")
@property ):
def app(self):return self.request.app return web.HTTPFound("/")
@property return await super()._iter()
def db(self):return self.app.db
@property @property
def services(self):return self.app.services def base_url(self):
async def json_response(B,data,**A):return web.json_response(data,**A) return str(self.request.url.with_path("").with_query(""))
@property
def session(self):return self.request.session @property
async def render_template(A,template_name,context=None): def app(self):
C=context;B=template_name return self.request.app
if B.endswith('.md'):D=await A.request.app.render_template(B,A.request,C);E=await render_markdown(A.app,D.body.decode());return web.Response(body=E,content_type='text/html')
return await A.request.app.render_template(B,A.request,C) @property
def db(self):
return self.app.db
@property
def services(self):
return self.app.services
async def json_response(self, data, **kwargs):
return web.json_response(data, **kwargs)
@property
def session(self):
return self.request.session
async def render_template(self, template_name, context=None):
if template_name.endswith(".md"):
response = await self.request.app.render_template(
template_name, self.request, context
)
body = await render_markdown(self.app, response.body.decode())
return web.Response(body=body, content_type="text/html")
return await self.request.app.render_template(
template_name, self.request, context
)
class BaseFormView(BaseView): class BaseFormView(BaseView):
form=None
async def get(A):B=A.form(app=A.app);return await A.json_response(await B.to_json()) form = None
async def post(A):
E='action';C=A.form(app=A.app);D=await A.request.json();C.set_user_data(D['form']);B=await C.to_json() async def get(self):
if D.get(E)=='validate':0 form = self.form(app=self.app)
if D.get(E)=='submit'and B['is_valid']:B=await A.submit(C);return await A.json_response(B)
return await A.json_response(B) return await self.json_response(await form.to_json())
async def submit(A,model=None):0
async def post(self):
form = self.form(app=self.app)
post = await self.request.json()
form.set_user_data(post["form"])
result = await form.to_json()
if post.get("action") == "validate":
# Pass
pass
if post.get("action") == "submit" and result["is_valid"]:
result = await self.submit(form)
return await self.json_response(result)
return await self.json_response(result)
async def submit(self, model=None):
pass
+81
View File
@@ -0,0 +1,81 @@
class WebSocketClient:
def __init__(self, hostname, port):
self.buffer = b''
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.hostname = hostname
self.port = port
self.connect()
def __getattr__(self, method, *args, **kwargs):
if method in self.__dict__.keys():
return self.__dict__[method]
def call(*args, **kwargs):
self.write(json.dumps({'method': method, 'args': args, 'kwargs': kwargs}))
return json.loads(self.read())
return call
def connect(self):
self.socket.connect((self.hostname, self.port))
key = base64.b64encode(b'1234123412341234').decode('utf-8')
handshake = (
f"GET /db HTTP/1.1\r\n"
f"Host: localhost:3131\r\n"
f"Upgrade: websocket\r\n"
f"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\n"
f"Sec-WebSocket-Version: 13\r\n\r\n"
)
self.socket.sendall(handshake.encode('utf-8'))
response = self.read_until(b'\r\n\r\n')
if b'101 Switching Protocols' not in response:
raise Exception("Failed to connect to WebSocket")
def write(self, message):
message_bytes = message.encode('utf-8')
length = len(message_bytes)
if length <= 125:
self.socket.sendall(b'\x81' + bytes([length]) + message_bytes)
elif length >= 126 and length <= 65535:
self.socket.sendall(b'\x81' + bytes([126]) + length.to_bytes(2, 'big') + message_bytes)
else:
self.socket.sendall(b'\x81' + bytes([127]) + length.to_bytes(8, 'big') + message_bytes)
def read_until(self, delimiter):
while True:
find_pos = self.buffer.find(delimiter)
if find_pos != -1:
data = self.buffer[:find_pos+4]
self.buffer = self.buffer[find_pos+4:]
return data
chunk = self.socket.recv(1024)
if not chunk:
return None
self.buffer += chunk
def read_exactly(self, length):
while len(self.buffer) < length:
chunk = self.socket.recv(length - len(self.buffer))
if not chunk:
return None
self.buffer += chunk
response = self.buffer[: length]
self.buffer = self.buffer[length:]
return response
def read(self):
frame = None
frame = self.read_exactly(2)
length = frame[1] & 127
if length == 126:
length = int.from_bytes(self.read_exactly(2), 'big')
elif length == 127:
length = int.from_bytes(self.read_exactly(8), 'big')
message = self.read_exactly(length)
return message
def close(self):
self.socket.close()

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