From 1616e4edb97284f705400c0598306202a083f60f Mon Sep 17 00:00:00 2001 From: retoor Date: Fri, 9 May 2025 14:57:22 +0200 Subject: [PATCH] revert 17c6124a57a394c63427a0038e598fdb40560f15 revert Minify. --- src/snek/__init__.py | 1 + src/snek/__main__.py | 39 +- src/snek/app.py | 364 ++++++++++--- src/snek/docs/app.py | 47 +- src/snek/dump.py | 45 +- src/snek/form/login.py | 61 ++- src/snek/form/register.py | 52 +- src/snek/form/search_user.py | 20 +- src/snek/form/settings/profile.py | 30 +- src/snek/gunicorn.py | 3 +- src/snek/mapper/__init__.py | 23 +- src/snek/mapper/channel.py | 6 +- src/snek/mapper/channel_member.py | 6 +- src/snek/mapper/channel_message.py | 6 +- src/snek/mapper/drive.py | 6 +- src/snek/mapper/drive_item.py | 7 +- src/snek/mapper/notification.py | 6 +- src/snek/mapper/repository.py | 6 +- src/snek/mapper/user.py | 21 +- src/snek/mapper/user_property.py | 6 +- src/snek/model/__init__.py | 25 +- src/snek/model/channel.py | 38 +- src/snek/model/channel_member.py | 58 ++- src/snek/model/channel_message.py | 19 +- src/snek/model/drive.py | 18 +- src/snek/model/drive_item.py | 27 +- src/snek/model/notification.py | 12 +- src/snek/model/repository.py | 15 +- src/snek/model/user.py | 75 ++- src/snek/model/user_property.py | 9 +- src/snek/service/__init__.py | 26 +- src/snek/service/channel.py | 149 ++++-- src/snek/service/channel_member.py | 98 +++- src/snek/service/channel_message.py | 122 +++-- src/snek/service/chat.py | 40 +- src/snek/service/drive.py | 190 +++++-- src/snek/service/drive_item.py | 22 +- src/snek/service/notification.py | 87 +++- src/snek/service/repository.py | 71 ++- src/snek/service/socket.py | 101 ++-- src/snek/service/user.py | 136 +++-- src/snek/service/user_property.py | 44 +- src/snek/service/util.py | 12 +- src/snek/sgit.py | 692 +++++++++++++++++-------- src/snek/system/cache.py | 205 +++++--- src/snek/system/form.py | 142 ++++- src/snek/system/http.py | 140 +++-- src/snek/system/mapper.py | 101 ++-- src/snek/system/markdown.py | 102 +++- src/snek/system/middleware.py | 64 ++- src/snek/system/model.py | 496 +++++++++++++----- src/snek/system/object.py | 18 +- src/snek/system/profiler.py | 55 +- src/snek/system/security.py | 99 +++- src/snek/system/service.py | 101 ++-- src/snek/system/template.py | 303 ++++++++--- src/snek/system/terminal.py | 158 ++++-- src/snek/system/view.py | 98 +++- src/snek/view/about.py | 38 +- src/snek/view/avatar.py | 44 +- src/snek/view/docs.py | 36 +- src/snek/view/drive.py | 346 +++++++++---- src/snek/view/index.py | 23 +- src/snek/view/login.py | 51 +- src/snek/view/login_form.py | 25 +- src/snek/view/logout.py | 64 ++- src/snek/view/register.py | 45 +- src/snek/view/register_form.py | 46 +- src/snek/view/rpc.py | 378 ++++++++++---- src/snek/view/search_user.py | 62 ++- src/snek/view/settings/index.py | 9 +- src/snek/view/settings/profile.py | 43 +- src/snek/view/settings/repositories.py | 107 ++-- src/snek/view/stats.py | 10 +- src/snek/view/status.py | 79 ++- src/snek/view/terminal.py | 69 ++- src/snek/view/threads.py | 44 +- src/snek/view/upload.py | 124 ++++- src/snek/view/user.py | 14 +- src/snek/view/web.py | 92 +++- src/snek/webdav.py | 512 +++++++++++++----- src/snekssh/app.py | 95 +++- src/snekssh/app2.py | 101 +++- src/snekssh/app3.py | 89 +++- src/snekssh/app4.py | 108 +++- src/snekssh/app5.py | 134 ++++- 86 files changed, 5826 insertions(+), 1885 deletions(-) diff --git a/src/snek/__init__.py b/src/snek/__init__.py index e69de29..8b13789 100644 --- a/src/snek/__init__.py +++ b/src/snek/__init__.py @@ -0,0 +1 @@ + diff --git a/src/snek/__main__.py b/src/snek/__main__.py index 5d861d9..35e56e3 100644 --- a/src/snek/__main__.py +++ b/src/snek/__main__.py @@ -1,21 +1,32 @@ -_D='Database path for the application' -_C='snek.db' -_B='--db_path' -_A=True -import click,uvloop +import click +import uvloop from aiohttp import web import asyncio from snek.app import Application from IPython import start_ipython + @click.group() -def cli():0 +def cli(): + pass + @cli.command() -@click.option('--port',default=8081,show_default=_A,help='Port to run the application on') -@click.option('--host',default='0.0.0.0',show_default=_A,help='Host to run the application on') -@click.option(_B,default=_C,show_default=_A,help=_D) -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) +@click.option('--port', default=8081, show_default=True, help='Port to run the application on') +@click.option('--host', default='0.0.0.0', show_default=True, help='Host to run the application on') +@click.option('--db_path', default='snek.db', show_default=True, help='Database path for the application') +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 + ) + @cli.command() -@click.option(_B,default=_C,show_default=_A,help=_D) -def shell(db_path):A=Application(db_path=f"sqlite:///{db_path}");start_ipython(argv=[],user_ns={'app':A}) -def main():cli() -if __name__=='__main__':main() \ No newline at end of file +@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() diff --git a/src/snek/app.py b/src/snek/app.py index f5f1948..ceb7c9d 100644 --- a/src/snek/app.py +++ b/src/snek/app.py @@ -1,31 +1,37 @@ -_G='name' -_F='static' -_E='user' -_D=None -_C=True -_B='channel_uid' -_A='uid' -import asyncio,logging,pathlib,time,uuid +import asyncio +import logging +import pathlib +import time +import uuid + from snek.view.threads import ThreadsView + logging.basicConfig(level=logging.DEBUG) + from concurrent.futures import ThreadPoolExecutor + 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 app.app import Application as BaseApplication from jinja2 import FileSystemLoader + from snek.docs.app import Application as DocsApplication from snek.mapper import get_mappers from snek.service import get_services from snek.system import http from snek.system.cache import Cache 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.template import EmojiExtension,LinkifyExtension,PythonExtension -from snek.view.about import AboutHTMLView,AboutMDView +from snek.system.template import EmojiExtension, LinkifyExtension, PythonExtension +from snek.view.about import AboutHTMLView, AboutMDView 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.index import IndexView from snek.view.login import LoginView @@ -42,79 +48,275 @@ from snek.view.settings.index import SettingsIndexView from snek.view.settings.profile import SettingsProfileView from snek.view.stats import StatsView 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.user import UserView from snek.view.web import WebView from snek.webdav import WebdavApplication from snek.sgit import GitApplication -SESSION_KEY=b'c79a0c5fda4b424189c427d28c9f7c34' + +SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34" + + @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 -async def trailing_slash_middleware(request,handler): - A=request - if A.path and not A.path.endswith('/'):raise web.HTTPFound(A.path+'/') - return await handler(A) +async def trailing_slash_middleware(request, handler): + if request.path and not request.path.endswith("/"): + # Redirect to the same path with a trailing slash + raise web.HTTPFound(request.path + "/") + return await handler(request) + + class Application(BaseApplication): - 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) - async def create_task(A,task):await A.tasks.put(task) - async def task_runner(A): - while _C: - 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() - except Exception as E:print(E) - A.db.commit() - async def prepare_database(A,app): - 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: - if not A.db[_E].has_index(E):A.db[_E].create_index(E,unique=_C) - if not A.db[D].has_index([_B,B]):A.db[D].create_index([_B,B]) - if not A.db[C].has_index([_B,B]):A.db[C].create_index([_B,B]) - except:pass - await app.services.drive.prepare_all();A.loop.create_task(A.task_runner()) - 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) - async def handle_test(A,request):return await A.render_template('test.html',request,context={_G:'retoor'}) - async def handle_http_get(C,request):A=request.query.get('url');B=await http.get(A);return web.Response(body=B) - 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'}) - async def render_template(A,template,request,context=_D): - I='channels';J='new_count';K='color';L=template;F='last_message_on';D=request;C=context;G=[] - if not C:C={} - C['rid']=str(uuid.uuid4()) - if D.session.get(_A): - async for E in A.services.channel_member.find(user_uid=D.session.get(_A),deleted_at=_D,is_banned=False): - 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 - if N:P=await N.get_user();O=P[K] - B[K]=O;B[F]=H[F];B['is_private']=H['tag']=='dm' - if M:B[_G]=M['nick'];B[_A]=E[_B] - else:B[_G]=E['label'];B[_A]=E[_B] - B[J]=E[J];G.append(B) - G.sort(key=lambda x:x[F]or'',reverse=_C) - if I not in C:C[I]=G - if _E not in C:C[_E]=await A.services.user.get(D.session.get(_A)) - 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): - D=request;E=D.match_info.get('filename','');C=[];F=D.session.get(_A) - if F: - A=await B.services.user.get_static_path(F) - if A:C.append(A) - for H in B.services.user.get_admin_uids(): - A=await B.services.user.get_static_path(H) - if A:C.append(A) - C.append(B.static_path) - for G in C: - if pathlib.Path(G).joinpath(E).exists():return web.FileResponse(pathlib.Path(G).joinpath(E)) - return web.HTTPNotFound() - async def get_user_template_loader(B,uid=_D): - C=[] - for D in B.services.user.get_admin_uids(): - A=await B.services.user.get_template_path(D) - if A:C.append(A) - if uid: - A=await B.services.user.get_template_path(uid) - if A:C.append(A) - C.append(B.template_path);return FileSystemLoader(C) -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()) \ No newline at end of file + + def __init__(self, *args, **kwargs): + middlewares = [ + cors_middleware, + web.normalize_path_middleware(merge_slashes=True), + ] + self.template_path = pathlib.Path(__file__).parent.joinpath("templates") + self.static_path = pathlib.Path(__file__).parent.joinpath("static") + super().__init__( + middlewares=middlewares, template_path=self.template_path, client_max_size=1024*1024*1024*5 *args, **kwargs + ) + session_setup(self, EncryptedCookieStorage(SESSION_KEY)) + self.tasks = asyncio.Queue() + self._middlewares.append(session_middleware) + self._middlewares.append(auth_middleware) + self.jinja2_env.add_extension(MarkdownExtension) + self.jinja2_env.add_extension(LinkifyExtension) + self.jinja2_env.add_extension(PythonExtension) + self.jinja2_env.add_extension(EmojiExtension) + + self.setup_router() + self.executor = None + self.cache = Cache(self) + self.services = get_services(app=self) + self.mappers = get_mappers(app=self) + self.on_startup.append(self.prepare_asyncio) + self.on_startup.append(self.prepare_database) + + 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: + task_start = time.time() + await task + task_end = time.time() + print(f"Task {task} took {task_end - task_start} seconds") + 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_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", 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}/{repo_name}", RepositoryView) + self.router.add_view("/repository/{username}/{repo_name}/{rel_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.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 + ): + item = {} + other_user = await self.services.channel_member.get_other_dm_user( + subscribed_channel["channel_uid"], request.session.get("uid") + ) + parent_object = await subscribed_channel.get_channel() + 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()) diff --git a/src/snek/docs/app.py b/src/snek/docs/app.py index b47df44..50a4245 100644 --- a/src/snek/docs/app.py +++ b/src/snek/docs/app.py @@ -1,14 +1,43 @@ import pathlib + from aiohttp import web from app.app import Application as BaseApplication + from snek.system.markdown import MarkdownExtension + + 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): - D='text/plain';E=b'Resource is not found on this server.';F='index.html';G=request;C=G.match_info['tail'].strip('/') - if C=='':C=F - A=B.path.joinpath(C) - if not A.exists():return web.Response(status=404,body=E,content_type=D) - if A.is_dir():A=A.joinpath(F) - 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 \ No newline at end of file + + def __init__(self, path=None, *args, **kwargs): + self.path = pathlib.Path(path) + template_path = self.path + + super().__init__(template_path=template_path, *args, **kwargs) + self.jinja2_env.add_extension(MarkdownExtension) + + 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 diff --git a/src/snek/dump.py b/src/snek/dump.py index 2d52196..b254756 100644 --- a/src/snek/dump.py +++ b/src/snek/dump.py @@ -1,12 +1,39 @@ -_B='created_at' -_A='uid' import asyncio + 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(): - A=[] - 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!') - print('Converting to json.');print('Converting succesful, now writing to dump.json') - with open('dump.txt','w')as C:C.write('\n\n'.join(A)) - print('Dump written to dump.json') -if __name__=='__main__':asyncio.run(dump_public_channels()) \ No newline at end of file + result = [] + for channel in app.db["channel"].find( + is_private=False, is_listed=True, tag="public" + ): + print(f"Dumping channel: {channel['label']}.") + 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()) diff --git a/src/snek/form/login.py b/src/snek/form/login.py index c0b8cfd..ef13d67 100644 --- a/src/snek/form/login.py +++ b/src/snek/form/login.py @@ -1,14 +1,51 @@ -_B='username' -_A='password' -from snek.system.form import Form,FormButtonElement,FormInputElement,HTMLElement +from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement + + class AuthField(FormInputElement): - @property - async def errors(self): - A=self;B=await super().errors - if A.model.password.value and A.model.username.value: - if not await A.app.services.user.validate_login(A.model.username.value,A.model.password.value):return['Invalid username or password'] - return B + + @property + async def errors(self): + result = await super().errors + if self.model.password.value and self.model.username.value: + 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): - 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 - async def is_valid(self):A=self;return all([A[_B],A[_A],not await A.username.errors,not await A.password.errors]) \ No newline at end of file + + title = HTMLElement(tag="h1", text="Login") + + 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, + ] + ) diff --git a/src/snek/form/register.py b/src/snek/form/register.py index a9f8c71..b105696 100644 --- a/src/snek/form/register.py +++ b/src/snek/form/register.py @@ -1,10 +1,44 @@ -_B='password' -_A='Register' -from snek.system.form import Form,FormButtonElement,FormInputElement,HTMLElement +from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement + + class UsernameField(FormInputElement): - @property - async def errors(self): - A=self;B=await super().errors - if A.value and await A.app.services.user.count(username=A.value):B.append('Username is not available.') - return B -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') \ No newline at end of file + + @property + async def errors(self): + result = await super().errors + if self.value and await self.app.services.user.count(username=self.value): + 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" + ) diff --git a/src/snek/form/search_user.py b/src/snek/form/search_user.py index bf6de66..7e946b9 100644 --- a/src/snek/form/search_user.py +++ b/src/snek/form/search_user.py @@ -1,2 +1,18 @@ -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') \ No newline at end of file +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" + ) diff --git a/src/snek/form/settings/profile.py b/src/snek/form/settings/profile.py index 094c28e..836cd67 100644 --- a/src/snek/form/settings/profile.py +++ b/src/snek/form/settings/profile.py @@ -1,5 +1,25 @@ -_C='button' -_B='submit' -_A='action' -from snek.system.form import Form,FormButtonElement,FormInputElement,HTMLElement -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) \ No newline at end of file +from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement + + +class SettingsProfileForm(Form): + + 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" + ) diff --git a/src/snek/gunicorn.py b/src/snek/gunicorn.py index b51f326..8583142 100644 --- a/src/snek/gunicorn.py +++ b/src/snek/gunicorn.py @@ -1,2 +1,3 @@ from snek.app import app -application=app \ No newline at end of file + +application = app diff --git a/src/snek/mapper/__init__.py b/src/snek/mapper/__init__.py index 917ab7d..ab7904f 100644 --- a/src/snek/mapper/__init__.py +++ b/src/snek/mapper/__init__.py @@ -1,4 +1,5 @@ import functools + from snek.mapper.channel import ChannelMapper from snek.mapper.channel_member import ChannelMemberMapper from snek.mapper.channel_message import ChannelMessageMapper @@ -9,6 +10,24 @@ from snek.mapper.user import UserMapper from snek.mapper.user_property import UserPropertyMapper from snek.mapper.repository import RepositoryMapper from snek.system.object import Object + + @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] \ No newline at end of file +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), + } + ) + + +def get_mapper(name, app=None): + return get_mappers(app=app)[name] diff --git a/src/snek/mapper/channel.py b/src/snek/mapper/channel.py index d663d5b..6239dc8 100644 --- a/src/snek/mapper/channel.py +++ b/src/snek/mapper/channel.py @@ -1,3 +1,7 @@ from snek.model.channel import ChannelModel from snek.system.mapper import BaseMapper -class ChannelMapper(BaseMapper):table_name='channel';model_class=ChannelModel \ No newline at end of file + + +class ChannelMapper(BaseMapper): + table_name = "channel" + model_class = ChannelModel diff --git a/src/snek/mapper/channel_member.py b/src/snek/mapper/channel_member.py index b221d99..f0f62d6 100644 --- a/src/snek/mapper/channel_member.py +++ b/src/snek/mapper/channel_member.py @@ -1,3 +1,7 @@ from snek.model.channel_member import ChannelMemberModel from snek.system.mapper import BaseMapper -class ChannelMemberMapper(BaseMapper):table_name='channel_member';model_class=ChannelMemberModel \ No newline at end of file + + +class ChannelMemberMapper(BaseMapper): + table_name = "channel_member" + model_class = ChannelMemberModel diff --git a/src/snek/mapper/channel_message.py b/src/snek/mapper/channel_message.py index c27a9cb..35ccbe9 100644 --- a/src/snek/mapper/channel_message.py +++ b/src/snek/mapper/channel_message.py @@ -1,3 +1,7 @@ from snek.model.channel_message import ChannelMessageModel from snek.system.mapper import BaseMapper -class ChannelMessageMapper(BaseMapper):model_class=ChannelMessageModel;table_name='channel_message' \ No newline at end of file + + +class ChannelMessageMapper(BaseMapper): + model_class = ChannelMessageModel + table_name = "channel_message" diff --git a/src/snek/mapper/drive.py b/src/snek/mapper/drive.py index cac318a..c92c687 100644 --- a/src/snek/mapper/drive.py +++ b/src/snek/mapper/drive.py @@ -1,3 +1,7 @@ from snek.model.drive import DriveModel from snek.system.mapper import BaseMapper -class DriveMapper(BaseMapper):table_name='drive';model_class=DriveModel \ No newline at end of file + + +class DriveMapper(BaseMapper): + table_name = "drive" + model_class = DriveModel diff --git a/src/snek/mapper/drive_item.py b/src/snek/mapper/drive_item.py index 96676f6..3d17a61 100644 --- a/src/snek/mapper/drive_item.py +++ b/src/snek/mapper/drive_item.py @@ -1,3 +1,8 @@ from snek.model.drive_item import DriveItemModel from snek.system.mapper import BaseMapper -class DriveItemMapper(BaseMapper):model_class=DriveItemModel;table_name='drive_item' \ No newline at end of file + + +class DriveItemMapper(BaseMapper): + + model_class = DriveItemModel + table_name = "drive_item" diff --git a/src/snek/mapper/notification.py b/src/snek/mapper/notification.py index c2372ce..9bd74b5 100644 --- a/src/snek/mapper/notification.py +++ b/src/snek/mapper/notification.py @@ -1,3 +1,7 @@ from snek.model.notification import NotificationModel from snek.system.mapper import BaseMapper -class NotificationMapper(BaseMapper):table_name='notification';model_class=NotificationModel \ No newline at end of file + + +class NotificationMapper(BaseMapper): + table_name = "notification" + model_class = NotificationModel diff --git a/src/snek/mapper/repository.py b/src/snek/mapper/repository.py index 1c04ba3..1ac10d4 100644 --- a/src/snek/mapper/repository.py +++ b/src/snek/mapper/repository.py @@ -1,3 +1,7 @@ from snek.model.repository import RepositoryModel from snek.system.mapper import BaseMapper -class RepositoryMapper(BaseMapper):model_class=RepositoryModel;table_name='repository' \ No newline at end of file + + +class RepositoryMapper(BaseMapper): + model_class = RepositoryModel + table_name = "repository" diff --git a/src/snek/mapper/user.py b/src/snek/mapper/user.py index 1df0eea..e0df494 100644 --- a/src/snek/mapper/user.py +++ b/src/snek/mapper/user.py @@ -1,7 +1,20 @@ from snek.model.user import UserModel from snek.system.mapper import BaseMapper + + class UserMapper(BaseMapper): - table_name='user';model_class=UserModel - def get_admin_uids(A): - 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[] \ No newline at end of file + table_name = "user" + model_class = UserModel + + 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 [] diff --git a/src/snek/mapper/user_property.py b/src/snek/mapper/user_property.py index 654e769..7359f60 100644 --- a/src/snek/mapper/user_property.py +++ b/src/snek/mapper/user_property.py @@ -1,3 +1,7 @@ from snek.model.user_property import UserPropertyModel from snek.system.mapper import BaseMapper -class UserPropertyMapper(BaseMapper):table_name='user_property';model_class=UserPropertyModel \ No newline at end of file + + +class UserPropertyMapper(BaseMapper): + table_name = "user_property" + model_class = UserPropertyModel diff --git a/src/snek/model/__init__.py b/src/snek/model/__init__.py index bb7fb2a..6399c89 100644 --- a/src/snek/model/__init__.py +++ b/src/snek/model/__init__.py @@ -1,6 +1,9 @@ import functools + from snek.model.channel import ChannelModel from snek.model.channel_member import ChannelMemberModel + +# from snek.model.channel_message import ChannelMessageModel from snek.model.channel_message import ChannelMessageModel from snek.model.drive import DriveModel from snek.model.drive_item import DriveItemModel @@ -9,6 +12,24 @@ from snek.model.user import UserModel from snek.model.user_property import UserPropertyModel from snek.model.repository import RepositoryModel from snek.system.object import Object + + @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_model(name):return get_models()[name] \ No newline at end of file +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_model(name): + return get_models()[name] diff --git a/src/snek/model/channel.py b/src/snek/model/channel.py index 939d658..0a90c39 100644 --- a/src/snek/model/channel.py +++ b/src/snek/model/channel.py @@ -1,12 +1,30 @@ -_C='uid' -_B=False -_A=True from snek.model.channel_message import ChannelMessageModel -from snek.system.model import BaseModel,ModelField +from snek.system.model import BaseModel, ModelField + + 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) - async def get_last_message(A): - try: - 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]) - except:pass - async def get_members(A):return await A.app.services.channel_member.find(channel_uid=A[_C],deleted_at=None,is_banned=_B) \ No newline at end of file + label = ModelField(name="label", required=True, kind=str) + description = ModelField(name="description", required=False, kind=str) + tag = ModelField(name="tag", required=False, kind=str) + created_by_uid = ModelField(name="created_by_uid", required=True, kind=str) + is_private = ModelField(name="is_private", required=True, kind=bool, value=False) + 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 + ) diff --git a/src/snek/model/channel_member.py b/src/snek/model/channel_member.py index 09b7e91..54b0418 100644 --- a/src/snek/model/channel_member.py +++ b/src/snek/model/channel_member.py @@ -1,19 +1,41 @@ -_D='channel_uid' -_C='user_uid' -_B=False -_A=True -from snek.system.model import BaseModel,ModelField +from snek.system.model import BaseModel, ModelField + + 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) - async def get_user(A):return await A.app.services.user.get(uid=A[_C]) - async def get_channel(A):return await A.app.services.channel.get(uid=A[_D]) - async def get_name(A): - B=await A.get_channel() - if B['tag']=='dm':C=await A.get_other_dm_user();return C['nick'] - return B['name']or A['label'] - async def get_other_dm_user(A): - B='uid';C=await A.get_channel() - if C['tag']!='dm':return - async for D in A.app.services.channel_member.find(channel_uid=C[B]): - if D[B]!=A[B]:return await A.app.services.user.get(uid=D[_C]) - return await A.get_user() \ No newline at end of file + label = ModelField(name="label", required=True, kind=str) + channel_uid = ModelField(name="channel_uid", required=True, kind=str) + user_uid = ModelField(name="user_uid", required=True, kind=str) + is_moderator = ModelField( + name="is_moderator", required=True, kind=bool, value=False + ) + is_read_only = ModelField( + name="is_read_only", required=True, kind=bool, value=False + ) + is_muted = ModelField(name="is_muted", required=True, kind=bool, value=False) + is_banned = ModelField(name="is_banned", required=True, kind=bool, value=False) + new_count = ModelField(name="new_count", required=False, kind=int, value=0) + + 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() diff --git a/src/snek/model/channel_message.py b/src/snek/model/channel_message.py index 0677d7c..524a8a4 100644 --- a/src/snek/model/channel_message.py +++ b/src/snek/model/channel_message.py @@ -1,8 +1,15 @@ -_B='user_uid' -_A='channel_uid' from snek.model.user import UserModel -from snek.system.model import BaseModel,ModelField +from snek.system.model import BaseModel, ModelField + + 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) - async def get_user(A):return await A.app.services.user.get(uid=A[_B]) - async def get_channel(A):return await A.app.services.channel.get(uid=A[_A]) \ No newline at end of file + channel_uid = ModelField(name="channel_uid", required=True, kind=str) + user_uid = ModelField(name="user_uid", required=True, kind=str) + message = ModelField(name="message", required=True, kind=str) + html = ModelField(name="html", required=False, kind=str) + + 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"]) diff --git a/src/snek/model/drive.py b/src/snek/model/drive.py index 62a2846..df17d0f 100644 --- a/src/snek/model/drive.py +++ b/src/snek/model/drive.py @@ -1,6 +1,14 @@ -from snek.system.model import BaseModel,ModelField +from snek.system.model import BaseModel, ModelField + + class DriveModel(BaseModel): - user_uid=ModelField(name='user_uid',required=True);name=ModelField(name='name',required=False,type=str) - @property - async def items(self): - async for A in self.app.services.drive_item.find(drive_uid=self['uid']):yield A \ No newline at end of file + + user_uid = ModelField(name="user_uid", required=True) + name = ModelField(name="name", required=False, type=str) + + @property + async def items(self): + async for drive_item in self.app.services.drive_item.find( + drive_uid=self["uid"] + ): + yield drive_item diff --git a/src/snek/model/drive_item.py b/src/snek/model/drive_item.py index a4427f3..e2b55b4 100644 --- a/src/snek/model/drive_item.py +++ b/src/snek/model/drive_item.py @@ -1,10 +1,21 @@ -_B='name' -_A=True import mimetypes -from snek.system.model import BaseModel,ModelField + +from snek.system.model import BaseModel, ModelField + + 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) - @property - def extension(self):return self[_B].split('.')[-1] - @property - def mime_type(self):A,B=mimetypes.guess_type(self[_B]);return A \ No newline at end of file + drive_uid = ModelField(name="drive_uid", required=True, kind=str) + name = ModelField(name="name", required=True, kind=str) + path = ModelField(name="path", required=True, kind=str) + file_type = ModelField(name="file_type", required=True, kind=str) + 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 diff --git a/src/snek/model/notification.py b/src/snek/model/notification.py index a8453eb..6a12328 100644 --- a/src/snek/model/notification.py +++ b/src/snek/model/notification.py @@ -1,3 +1,9 @@ -_A=True -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) \ No newline at end of file +from snek.system.model import BaseModel, ModelField + + +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) diff --git a/src/snek/model/repository.py b/src/snek/model/repository.py index 40ef94a..598cbb2 100644 --- a/src/snek/model/repository.py +++ b/src/snek/model/repository.py @@ -1,3 +1,14 @@ from snek.model.user import UserModel -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) \ No newline at end of file +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) + + + diff --git a/src/snek/model/user.py b/src/snek/model/user.py index 8572402..9869456 100644 --- a/src/snek/model/user.py +++ b/src/snek/model/user.py @@ -1,17 +1,60 @@ -_D='^[a-zA-Z0-9_-+/]+$' -_C=False -_B=True -_A='uid' -from snek.system.model import BaseModel,ModelField +from snek.system.model import BaseModel, ModelField + + 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): - B=await A.app.services.user_property.find_one(user_uid=A[_A],name=name) - if B:return B['value'] - async def has_property(A,name):return await A.app.services.user_property.exists(user_uid=A[_A],name=name) - async def set_property(A,name,value): - C=value;B=name - 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) - async def get_channel_members(A): - async for B in A.app.services.channel_member.find(user_uid=A[_A],is_banned=_C,deleted_at=None):yield B \ No newline at end of file + + username = ModelField( + name="username", + required=True, + min_length=2, + max_length=20, + regex=r"^[a-zA-Z0-9_-+/]+$", + ) + nick = ModelField( + name="nick", + 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 diff --git a/src/snek/model/user_property.py b/src/snek/model/user_property.py index 77e5b25..1231423 100644 --- a/src/snek/model/user_property.py +++ b/src/snek/model/user_property.py @@ -1,2 +1,7 @@ -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) \ No newline at end of file +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) diff --git a/src/snek/service/__init__.py b/src/snek/service/__init__.py index 583ef6c..be356dc 100644 --- a/src/snek/service/__init__.py +++ b/src/snek/service/__init__.py @@ -1,4 +1,5 @@ import functools + from snek.service.channel import ChannelService from snek.service.channel_member import ChannelMemberService from snek.service.channel_message import ChannelMessageService @@ -12,6 +13,27 @@ from snek.service.user_property import UserPropertyService from snek.service.util import UtilService from snek.service.repository import RepositoryService from snek.system.object import Object + + @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_service(name,app=None):return get_services(app=app)[name] \ No newline at end of file +def get_services(app): + 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), + } + ) + + +def get_service(name, app=None): + return get_services(app=app)[name] diff --git a/src/snek/service/channel.py b/src/snek/service/channel.py index 8c39f6e..b90e66f 100644 --- a/src/snek/service/channel.py +++ b/src/snek/service/channel.py @@ -1,49 +1,108 @@ -_F='channel_uid' -_E='public' -_D=True -_C='uid' -_B=None -_A=False from datetime import datetime + from snek.system.model import now from snek.system.service import BaseService + + class ChannelService(BaseService): - mapper_name='channel' - async def get(E,uid=_B,**A): - D='name';C=uid - if C: - A[_C]=C;B=await super().get(**A) - if B:return B - del A[_C];A[D]=C;B=await super().get(**A) - if B:return B - A[D]='#'+C;B=await super().get(**A) - if B:return B - return - return await super().get(**A) - async def create(C,label,created_by_uid,description=_B,tag=_B,is_private=_A,is_listed=_D): - E=is_listed;D=tag;B=label - if B[0]!='#'and E:B=f"#{B}" - F=await C.count(deleted_at=_B) - if not D and not F:D=_E - 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 - if await C.save(A):return A - raise Exception(f"Failed to create channel: {A.errors}.") - async def get_dm(A,user1,user2): - C=user2;B=user1;D=await A.services.channel_member.get_dm(B,C) - if D:return await A.get(uid=D[_F]) - E=await A.create('DM',B,tag='dm');await A.services.channel_member.create_dm(E[_C],B,C);return E - async def get_users(A,channel_uid): - async for C in A.services.channel_member.find(channel_uid=channel_uid,is_banned=_A,is_muted=_A,deleted_at=_B): - B=await A.services.user.get(uid=C['user_uid']) - if B:yield B - async def get_online_users(C,channel_uid): - B='last_ping' - async for A in C.get_users(channel_uid): - if not A[B]:continue - if(datetime.fromisoformat(now())-datetime.fromisoformat(A[B])).total_seconds()<20:yield A - async def get_for_user(A,user_uid): - 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 - async def ensure_public_channel(B,created_by_uid): - 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) - 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 \ No newline at end of file + mapper_name = "channel" + + async def get(self, uid=None, **kwargs): + if uid: + kwargs["uid"] = uid + result = await super().get(**kwargs) + if result: + return result + del kwargs["uid"] + kwargs["name"] = uid + result = await super().get(**kwargs) + if result: + return result + kwargs["name"] = "#" + uid + result = await super().get(**kwargs) + if result: + return result + return None + return await super().get(**kwargs) + + async def create( + self, + label, + created_by_uid, + description=None, + tag=None, + is_private=False, + is_listed=True, + ): + if label[0] != "#" and is_listed: + 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_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() < 20: + 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 diff --git a/src/snek/service/channel_member.py b/src/snek/service/channel_member.py index cd3a62b..df96786 100644 --- a/src/snek/service/channel_member.py +++ b/src/snek/service/channel_member.py @@ -1,28 +1,74 @@ -_C='user_uid' -_B='channel_uid' -_A=False from snek.system.service import 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) - 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 create(B,channel_uid,user_uid,is_moderator=_A,is_read_only=_A,is_muted=_A,is_banned=_A): - D='label';E='is_banned';F=user_uid;C=channel_uid;A=await B.get(channel_uid=C,user_uid=F) - if A: - if A[E]:return _A - return A - 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 - if await B.save(A):return A - raise Exception(f"Failed to create channel member: {A.errors}.") - async def get_dm(D,from_user,to_user): - E='to_user';F='from_user';A=to_user;B=from_user - 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 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 - async def get_other_dm_user(A,channel_uid,user_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]) - if F['tag']!='dm':return - async for E in A.services.channel_member.find(channel_uid=C): - if E[B]!=D[B]:return await A.services.user.get(uid=E[_C]) - 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 \ No newline at end of file + + mapper_name = "channel_member" + + async def mark_as_read(self, channel_uid, user_uid): + channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid) + channel_member["new_count"] = 0 + return await self.save(channel_member) + + async def get_user_uids(self, channel_uid): + async for model in self.mapper.query( + "SELECT user_uid FROM channel_member WHERE channel_uid=:channel_uid", + {"channel_uid": channel_uid}, + ): + yield model["user_uid"] + + async def create( + self, + channel_uid, + user_uid, + is_moderator=False, + is_read_only=False, + is_muted=False, + 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 diff --git a/src/snek/service/channel_message.py b/src/snek/service/channel_message.py index 1841024..f8a000f 100644 --- a/src/snek/service/channel_message.py +++ b/src/snek/service/channel_message.py @@ -1,33 +1,93 @@ -_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 + + class ChannelMessageService(BaseService): - 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]}) - try:G=B.app.jinja2_env.get_template('message.html');A[_G]=G.render(**D) - except Exception as H:print(H,flush=True) - if await B.save(A):return A - raise Exception(f"Failed to create channel message: {A.errors}.") - async def to_extended_dict(C,message): - A=message;B=await C.services.user.get(uid=A[_B]) - 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]} - 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 - try: - if G: - 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) - 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) - else: - 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) - except:pass - A.sort(key=lambda x:x[_H]);return A \ No newline at end of file + mapper_name = "channel_message" + + async def create(self, channel_uid, user_uid, message): + model = await self.new() + + model["channel_uid"] = channel_uid + model["user_uid"] = user_uid + model["message"] = message + + context = {} + + record = model.record + context.update(record) + user = await self.app.services.user.get(uid=user_uid) + context.update( + { + "user_uid": user["uid"], + "username": user["username"], + "user_nick": user["nick"], + "color": user["color"], + } + ) + 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 self.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 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 diff --git a/src/snek/service/chat.py b/src/snek/service/chat.py index 14a9ad1..388d5c0 100644 --- a/src/snek/service/chat.py +++ b/src/snek/service/chat.py @@ -1,7 +1,39 @@ from snek.system.model import now from snek.system.service import 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) - if not F:raise Exception('Channel not found.') - 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 \ No newline at end of file + + async def send(self, user_uid, channel_uid, message): + 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 + ) + 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"], + }, + ) + await self.app.create_task( + self.services.notification.create_channel_message(channel_message_uid) + ) + + return True diff --git a/src/snek/service/drive.py b/src/snek/service/drive.py index e38b3fa..38035c7 100644 --- a/src/snek/service/drive.py +++ b/src/snek/service/drive.py @@ -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 + + 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): - A=extension - if A.startswith('.'):A=A[1:] - if A in B.EXTENSIONS_PICTURES:return _E - if A in B.EXTENSIONS_VIDEOS:return _F - if A in B.EXTENSIONS_ARCHIVES:return _G - if A in B.EXTENSIONS_AUDIO:return'Audio' - if A in B.EXTENSIONS_DOCS:return _H - return _B - 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) - async def get_by_user(C,user_uid,name=None): - B=name;D={_C:user_uid} - async for A in C.find(**D): - if not B:yield A - elif A[_A]==B:yield A - elif not A[_A]and B==_B:A[_A]=_B;await C.save(A);yield A - async def get_or_create(B,user_uid,name=None,extensions=None): - D=user_uid;C=name;E={_C:D} - if C:E[_A]=C - async for A in B.get_by_user(**E):return A - A=await B.new();A[_C]=D;A[_A]=C;await B.save(A);return A - async def prepare_default_drives(B): - C='drive_uid' - async for A in B.services.drive_item.find(): - E=A.extension;D=await B.get_drive_by_extension(A[_C],E) - if not A[C]==D[_D]:A[C]=D[_D];await B.services.drive_item.save(A) - 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) - async def prepare_all(A): - await A.prepare_default_drives() - async for B in A.services.user.find():await A.prepare_default_drives_for_user(B[_D]) \ No newline at end of file + + 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(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"]) diff --git a/src/snek/service/drive_item.py b/src/snek/service/drive_item.py index 0740949..ce747c1 100644 --- a/src/snek/service/drive_item.py +++ b/src/snek/service/drive_item.py @@ -1,7 +1,19 @@ from snek.system.service import BaseService + + class DriveItemService(BaseService): - mapper_name='drive_item' - async def create(B,drive_uid,name,path,type_,size): - 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 - C=await A.errors;raise Exception(f"Failed to create drive item: {C}.") \ No newline at end of file + + mapper_name = "drive_item" + + async def create(self, drive_uid, name, path, type_, size): + 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 + raise Exception(f"Failed to create drive item: {errors}.") diff --git a/src/snek/service/notification.py b/src/snek/service/notification.py index 968d426..a22e8ae 100644 --- a/src/snek/service/notification.py +++ b/src/snek/service/notification.py @@ -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.service import BaseService + + class NotificationService(BaseService): - 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) - if not A:return _B - A['read_at']=now();await B.save(A);return True - 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}) - async def create(B,object_uid,object_type,user_uid,message): - A=await B.new();A[_C]=object_uid;A[_D]=object_type;A[_A]=user_uid;A[_E]=message - if await B.save(A):return A - raise Exception(f"Failed to create notification: {A.errors}.") - async def create_channel_message(A,channel_message_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() - async for B in A.services.channel_member.find(channel_uid=F['channel_uid'],is_banned=_B,is_muted=_B,deleted_at=None): - if not B[D]:B[D]=0 - 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"]}." - try:await A.save(C) - except Exception:raise Exception(f"Failed to create notification: {C.errors}.") - A.app.db.commit() \ No newline at end of file + mapper_name = "notification" + + async def mark_as_read(self, user_uid, channel_message_uid): + model = await self.get(user_uid, object_uid=channel_message_uid) + if not model: + return False + model["read_at"] = now() + await self.save(model) + return True + + async def get_unread_stats(self, user_uid): + await self.query( + "SELECT object_type, COUNT(*) as count FROM notification WHERE user_uid=:user_uid AND read_at IS NULL GROUP BY object_type", + {"user_uid": user_uid}, + ) + + async def create(self, object_uid, object_type, user_uid, message): + model = await self.new() + model["object_uid"] = object_uid + 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() diff --git a/src/snek/service/repository.py b/src/snek/service/repository.py index be30602..120c232 100644 --- a/src/snek/service/repository.py +++ b/src/snek/service/repository.py @@ -1,23 +1,52 @@ -_B='user_uid' -_A=False from snek.system.service import BaseService -import asyncio,shutil +import asyncio +import shutil + class RepositoryService(BaseService): - 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) - try:await C.run_in_executor(None,shutil.rmtree,D) - except Exception as E:print(E) - await super().delete(user_uid=A,name=name) - async def exists(B,user_uid,name,**A):A[_B]=user_uid;A['name']=name;return await super().exists(**A) - async def init(D,user_uid,name): - B='.git';A=await D.services.user.get_repository_path(user_uid) - if not A.exists():A.mkdir(parents=True) - A=A.joinpath(name);A=str(A) - 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): - C=name;D=user_uid - if await A.exists(user_uid=D,name=C):return _A - if not await A.init(user_uid=D,name=C):return _A - B=await A.new();B[_B]=D;B['name']=C;B['is_private']=is_private;return await A.save(B) \ No newline at end of file + mapper_name = "repository" + + async def delete(self, user_uid, name): + loop = asyncio.get_event_loop() + repository_path = (await self.services.user.get_repository_path(user_uid)).joinpath(name) + try: + await loop.run_in_executor(None, shutil.rmtree, repository_path) + except Exception as ex: + print(ex) + + await super().delete(user_uid=user_uid, name=name) + + + async def exists(self, user_uid, name, **kwargs): + kwargs["user_uid"] = user_uid + kwargs["name"] = name + return await super().exists(**kwargs) + + 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) diff --git a/src/snek/service/socket.py b/src/snek/service/socket.py index 86c83a8..a3654d2 100644 --- a/src/snek/service/socket.py +++ b/src/snek/service/socket.py @@ -1,36 +1,71 @@ -_B=False -_A=True from snek.model.user import UserModel from snek.system.service import BaseService + + class SocketService(BaseService): - class Socket: - def __init__(A,ws,user):A.ws=ws;A.is_connected=_A;A.user=user - async def send_json(A,data): - if not A.is_connected:return _B - try:await A.ws.send_json(data) - except Exception:A.is_connected=_B - return A.is_connected - async def close(A): - if not A.is_connected:return _A - await A.ws.close();A.is_connected=_B;return _A - def __init__(A,app):super().__init__(app);A.sockets=set();A.users={};A.subscriptions={} - async def add(A,ws,user_uid): - B=user_uid;C=A.Socket(ws,await A.app.services.user.get(uid=B));A.sockets.add(C) - if not A.users.get(B):A.users[B]=set() - A.users[B].add(C) - async def subscribe(A,ws,channel_uid,user_uid): - B=channel_uid - if B not in A.subscriptions:A.subscriptions[B]=set() - C=A.Socket(ws,await A.app.services.user.get(uid=user_uid));A.subscriptions[B].add(C) - async def send_to_user(B,user_uid,message): - A=0 - for C in B.users.get(user_uid,[]): - if await C.send_json(message):A+=1 - return A - async def broadcast(A,channel_uid,message): - 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) - except Exception as C:print(C,flush=_A) - return _A - async def delete(A,ws): - for B in[A for A in A.sockets if A.ws==ws]:await B.close();A.sockets.remove(B) \ No newline at end of file + + class Socket: + def __init__(self, ws, user: UserModel): + self.ws = ws + self.is_connected = True + self.user = user + + async def send_json(self, data): + if not self.is_connected: + return False + try: + await self.ws.send_json(data) + except Exception: + self.is_connected = False + return self.is_connected + + async def close(self): + if not self.is_connected: + return True + + await self.ws.close() + self.is_connected = False + + return True + + def __init__(self, app): + super().__init__(app) + self.sockets = set() + self.users = {} + self.subscriptions = {} + + async def add(self, ws, user_uid): + s = self.Socket(ws, await self.app.services.user.get(uid=user_uid)) + self.sockets.add(s) + 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): + try: + async for user_uid in self.services.channel_member.get_user_uids( + channel_uid + ): + print(user_uid, flush=True) + await self.send_to_user(user_uid, message) + except Exception as ex: + print(ex, flush=True) + return True + + async def delete(self, ws): + for s in [sock for sock in self.sockets if sock.ws == ws]: + await s.close() + self.sockets.remove(s) diff --git a/src/snek/service/user.py b/src/snek/service/user.py index 6ece3fd..76e6d1c 100644 --- a/src/snek/service/user.py +++ b/src/snek/service/user.py @@ -1,53 +1,91 @@ -_B='color' -_A=True import pathlib + from snek.system import security from snek.system.service import BaseService + + class UserService(BaseService): - mapper_name='user' - async def get_by_username(A,username):return await A.get(username=username) - async def search(C,query,**D): - A=query;A=A.strip().lower() - if not A:return[] - B=[] - async for E in C.find(username={'ilike':'%'+A+'%'},**D):B.append(E) - return B - async def validate_login(C,username,password): - A=False;B=await C.get(username=username) - if not B:return A - if not await security.verify(password,B['password']):return A - return _A - async def save(B,user): - A=user - if not A[_B]:A[_B]=await B.services.util.random_light_hex_color() - return await super().save(A) - async def authenticate(B,username,password): - C=password;A=username;print(A,C,flush=_A);D=await B.validate_login(A,C);print(D,flush=_A) - if not D:return - E=await B.get(username=A,deleted_at=None);return E - 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 get_static_path(B,user_uid): - A=pathlib.Path(f"./drive/{user_uid}/snek/static") - if not A.exists():return - return A - async def get_template_path(B,user_uid): - A=pathlib.Path(f"./drive/{user_uid}/snek/templates") - if not A.exists():return - return A - async def get_home_folder(B,user_uid): - A=pathlib.Path(f"./drive/{user_uid}") - if not A.exists(): - try:A.mkdir(parents=_A,exist_ok=_A) - except:pass - return A - async def register(B,email,username,password): - C=username - if await B.exists(username=C):raise Exception('User already exists.') - 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): - if A: - D=await B.services.channel.ensure_public_channel(A['uid']) - if not D:raise Exception('Failed to create public channel.') - return A - raise Exception(f"Failed to create user: {A.errors}.") \ No newline at end of file + mapper_name = "user" + + async def get_by_username(self, username): + return await self.get(username=username) + + async def search(self, query, **kwargs): + query = query.strip().lower() + if not query: + return [] + results = [] + async for result in self.find(username={"ilike": "%" + query + "%"}, **kwargs): + results.append(result) + return results + + async def validate_login(self, username, password): + model = await self.get(username=username) + if not model: + return False + if not await security.verify(password, model["password"]): + return False + return True + + async def save(self, user): + if not user["color"]: + user["color"] = await self.services.util.random_light_hex_color() + return await super().save(user) + + async def authenticate(self, username, password): + print(username, password, flush=True) + success = await self.validate_login(username, password) + print(success, flush=True) + if not success: + return None + + model = await self.get(username=username, deleted_at=None) + return model + + def get_admin_uids(self): + return self.mapper.get_admin_uids() + + 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 + + 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}.") diff --git a/src/snek/service/user_property.py b/src/snek/service/user_property.py index da9136a..4d11fa8 100644 --- a/src/snek/service/user_property.py +++ b/src/snek/service/user_property.py @@ -1,15 +1,35 @@ -_A='user_property' import json + from snek.system.service import BaseService + + class UserPropertyService(BaseService): - mapper_name=_A - 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): - try:return json.loads((await super().get(user_uid=user_uid,name=name))['value']) - except Exception as A:print(A);return - async def search(C,query,**D): - A=query;A=A.strip().lower() - if not A:raise[] - B=[] - async for E in C.find(name={'ilike':'%'+A+'%'},**D):B.append(E) - return B \ No newline at end of file + mapper_name = "user_property" + + async def set(self, user_uid, name, value): + self.mapper.db["user_property"].upsert( + { + "user_uid": user_uid, + "name": name, + "value": json.dumps(value, default=str), + }, + ["user_uid", "name"], + ) + + 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 diff --git a/src/snek/service/util.py b/src/snek/service/util.py index 73dbec3..b620d9c 100644 --- a/src/snek/service/util.py +++ b/src/snek/service/util.py @@ -1,4 +1,14 @@ import random + from snek.system.service import 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}" \ No newline at end of file + + 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}" diff --git a/src/snek/sgit.py b/src/snek/sgit.py index 0f3a69f..f8bfeb7 100644 --- a/src/snek/sgit.py +++ b/src/snek/sgit.py @@ -1,207 +1,489 @@ -_O='branches' -_N='message' -_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 +import os +import aiohttp 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') -logger=logging.getLogger('git_server') +import git +import shutil +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): - 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)]) - async def check_basic_auth(B,request): - C='Basic ';A=request;D=A.headers.get('Authorization','') - if not D.startswith(C):return _E,_E - 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) - if not A[_D]:return _E,_E - A[_A]=await B.parent.services.user.get_repository_path(A[_D]['uid']);return A[_D][_B],A[_A] - @staticmethod - def require_auth(handler): - async def A(self,request,*D,**E): - A=request;B,C=await self.check_basic_auth(A) - if not B or not C:return web.Response(status=401,headers={'WWW-Authenticate':'Basic'},text='Authentication required') - A[_B]=B;A[_A]=C;return await handler(self,A,*D,**E) - return A - def repo_path(A,repository_path,repo_name):return repository_path.joinpath(repo_name+_F) - def check_repo_exists(A,repository_path,repo_name): - B=A.repo_path(repository_path,repo_name) - if not os.path.exists(B):return web.Response(text=_J,status=404) - @require_auth - async def create_repository(self,request): - B=request;E=B[_B];A=B.match_info[_C];F=B[_A] - if not A or'/'in A or'..'in A:return web.Response(text='Invalid repository name',status=400) - C=self.repo_path(F,A) - 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 delete_repository(self,request): - B=request;F=B[_B];A=B.match_info[_C];C=B[_A];D=self.check_repo_exists(C,A) - if D:return D - 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}") - 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) - @require_auth - async def clone_repository(self,request): - A=request;H=A[_B];B=A.match_info[_C];E=A[_A];C=self.check_repo_exists(E,B) - 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) - @require_auth - async def push_repository(self,request): - B=request;L=B[_B];C=B.match_info[_C];E=B[_A];F=self.check_repo_exists(E,C) - if F:return F - try:D=await B.json() - except json.JSONDecodeError:return web.Response(text=_L,status=400) - 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: - A=git.Repo.clone_from(self.repo_path(E,C),I) - for J in H: - K=os.path.join(I,J.get('file',''));N=J.get('content','');os.makedirs(os.path.dirname(K),exist_ok=True) - with open(K,'w')as O:O.write(N) - A.git.add(A=True) - 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() - A.index.commit(M);P=A.remote(_K);P.push(refspec=f"{G}:{G}") - logger.info(f"Pushed to repository: {C} for user {L}");return web.Response(text=f"Successfully pushed changes to {C}") - @require_auth - 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) - if I:return I - try:E=await C.json() - except json.JSONDecodeError:E={} - B=E.get('remote_url');L=E.get(_G,_I) - if not B:return web.Response(text='Remote URL is required',status=400) - with tempfile.TemporaryDirectory()as M: - try: - D=git.Repo.clone_from(self.repo_path(H,A),M);F='pull_source' - try:G=D.create_remote(F,B) - except git.GitCommandError:G=D.remote(F);G.set_url(B) - 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}") - 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) - @require_auth - async def status_repository(self,request): - C=request;S=C[_B];B=C.match_info[_C];F=C[_A];G=self.check_repo_exists(F,B) - if G:return G - with tempfile.TemporaryDirectory()as D: - try: - 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=[] - 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}) - I=[] - for(J,T,N)in os.walk(D): - if _F in J:continue - for O in N:P=os.path.join(J,O);Q=os.path.relpath(P,D);I.append(Q) - R={_H:B,_O:L,'active_branch':M,'recent_commits':H,'files':I};return web.json_response(R) - 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 - async def list_repositories(self,request): - D=request;G=D[_B] - try: - A=[];B=self.REPO_DIR - if os.path.exists(B): - for C in os.listdir(B): - F=os.path.join(B,C) - 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}) - else:return web.Response(text='\n'.join(A)if A 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): - A=request;H=A[_B];B=A.match_info[_C];C=A[_A];D=self.check_repo_exists(C,B) - if D:return D - 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 - async def create_branch(self,request): - B=request;I=B[_B];C=B.match_info[_C];D=B[_A];E=self.check_repo_exists(D,C) - if E:return E - try:F=await B.json() - except json.JSONDecodeError:return web.Response(text=_L,status=400) - A=F.get('branch_name');J=F.get('start_point','HEAD') - if not A:return web.Response(text='Branch name is required',status=400) - with tempfile.TemporaryDirectory()as K: - 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}") - 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) - @require_auth - async def commit_log(self,request): - B=request;L=B[_B];C=B.match_info[_C];F=B[_A];G=self.check_repo_exists(F,C) - if G:return G - try:I=int(B.query.get('limit',10));H=B.query.get(_G,_I) - except ValueError:return web.Response(text='Invalid limit parameter',status=400) - with tempfile.TemporaryDirectory()as J: - try: - K=git.Repo.clone_from(self.repo_path(F,C),J);E=[] - try: - 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()}) - except git.GitCommandError as D: - if'unknown revision or path'in str(D):E=[] - else:raise - return web.json_response({_H:C,_G:H,'commits':E}) - 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) - @require_auth - async def file_content(self,request): - 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) - if G:return G - with tempfile.TemporaryDirectory()as H: - try: - J=git.Repo.clone_from(self.repo_path(F,B),H) - try:J.git.checkout(E) - except git.GitCommandError:return web.Response(text=f"Branch '{E}' not found",status=404) - D=os.path.join(H,C) - if not os.path.exists(D):return web.Response(text=f"File '{C}' not found",status=404) - if os.path.isdir(D):K=os.listdir(D);return web.json_response({_H:B,'path':C,'type':'directory','contents':K}) - else: - try: - with open(D,'r')as L:M=L.read() - return web.Response(text=M) - except UnicodeDecodeError:return web.Response(text=f"Cannot display binary file content for '{C}'",status=400) - 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) - @require_auth - async def git_smart_http(self,request): - 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 - async def K(): - B=C.lstrip('/') - if B.endswith(F):A=B[:-len(F)] - elif B.endswith(E):A=B[:-len(E)] - elif B.endswith(D):A=B[:-len(D)] - else:A=B - if A.endswith(_F):A=A[:-4] - A=A[4:];G=N.joinpath(A+_F);logger.info(f"Resolved repo path: {G}");return G - async def O(service): - C=service;D=await K();logger.info(f"handle_info_refs: {D}") - if not os.path.exists(D):return web.Response(text=_J,status=404) - L=[C,J,'--advertise-refs',str(D)] - try: - E=await asyncio.create_subprocess_exec(*L,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.PIPE);M,F=await E.communicate() - if E.returncode!=0:logger.error(f"Git command failed: {F.decode()}");return web.Response(text=f"Git error: {F.decode()}",status=500) - 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 - except Exception as H:logger.error(f"Error handling info/refs: {str(H)}");return web.Response(text=f"Server error: {str(H)}",status=500) - async def L(service): - B=service;C=await K();logger.info(f"handle_service_rpc: {C}") - if not os.path.exists(C):return web.Response(text=_J,status=404) - if not A.headers.get(I)==f"application/x-{B}-request":return web.Response(text='Invalid Content-Type',status=403) - G=await A.read();H=[B,J,str(C)] - try: - 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) - if D.returncode!=0:logger.error(f"Git command failed: {E.decode()}");return web.Response(text=f"Git error: {E.decode()}",status=500) - return web.Response(body=L,content_type=f"application/x-{B}-result") - except Exception as F:logger.error(f"Error handling service RPC: {str(F)}");return web.Response(text=f"Server error: {str(F)}",status=500) - if A.method=='GET'and C.endswith(F): - M=A.query.get('service') - if M in(H,G):return await O(M) - else:return web.Response(text='Smart HTTP requires service parameter',status=400) - elif A.method==B and E in C:return await L(H) - elif A.method==B and D in C:return await L(G) - 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) \ No newline at end of file + def __init__(self, parent=None): + self.parent = parent + super().__init__(client_max_size=1024*1024*1024*5) + self.REPO_DIR = "drive/repositories/3177f85e-dbb3-4406-993e-3d3748fea545" + self.USERS = { + 'x': 'x', + 'bob': 'bobpass', + } + self.add_routes([ + web.post('/create/{repo_name}', self.create_repository), + web.delete('/delete/{repo_name}', self.delete_repository), + web.get('/clone/{repo_name}', self.clone_repository), + web.post('/push/{repo_name}', self.push_repository), + web.post('/pull/{repo_name}', self.pull_repository), + web.get('/status/{repo_name}', self.status_repository), + web.get('/list', self.list_repositories), + web.get('/branches/{repo_name}', self.list_branches), + web.post('/branches/{repo_name}', self.create_branch), + web.get('/log/{repo_name}', self.commit_log), + web.get('/file/{repo_name}/{file_path:.*}', self.file_content), + web.get('/{path:.+}/info/refs', self.git_smart_http), + web.post('/{path:.+}/git-upload-pack', self.git_smart_http), + web.post('/{path:.+}/git-receive-pack', self.git_smart_http), + web.get('/{repo_name}.git/info/refs', self.git_smart_http), + web.post('/{repo_name}.git/git-upload-pack', self.git_smart_http), + web.post('/{repo_name}.git/git-receive-pack', self.git_smart_http), + ]) + + + async def check_basic_auth(self, request): + auth_header = request.headers.get("Authorization", "") + if not auth_header.startswith("Basic "): + return None,None + encoded_creds = auth_header.split("Basic ")[1] + decoded_creds = base64.b64decode(encoded_creds).decode() + username, password = decoded_creds.split(":", 1) + request["user"] = await self.parent.services.user.authenticate( + username=username, password=password + ) + if not request["user"]: + return None,None + request["repository_path"] = await self.parent.services.user.get_repository_path( + request["user"]["uid"] + ) + + return request["user"]['username'],request["repository_path"] + + + @staticmethod + def require_auth(handler): + async def wrapped(self, request, *args, **kwargs): + username, repository_path = await self.check_basic_auth(request) + if not username or not repository_path: + return web.Response(status=401, headers={'WWW-Authenticate': 'Basic'}, text='Authentication required') + request['username'] = username + request['repository_path'] = repository_path + return await handler(self, request, *args, **kwargs) + return wrapped + + def repo_path(self, repository_path, repo_name): + return repository_path.joinpath(repo_name + '.git') + + def check_repo_exists(self, repository_path, repo_name): + repo_dir = self.repo_path(repository_path, repo_name) + if not os.path.exists(repo_dir): + return web.Response(text="Repository not found", status=404) + return None + + @require_auth + async def create_repository(self, request): + username = request['username'] + repo_name = request.match_info['repo_name'] + repository_path = request['repository_path'] + if not repo_name or '/' in repo_name or '..' in repo_name: + return web.Response(text="Invalid repository name", status=400) + repo_dir = self.repo_path(repository_path, repo_name) + if os.path.exists(repo_dir): + return web.Response(text="Repository already exists", status=400) + try: + git.Repo.init(repo_dir, bare=True) + logger.info(f"Created repository: {repo_name} for user {username}") + return web.Response(text=f"Created repository {repo_name}") + except Exception as e: + logger.error(f"Error creating repository {repo_name}: {str(e)}") + return web.Response(text=f"Error creating repository: {str(e)}", status=500) + + @require_auth + async def delete_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 + #''' + try: + shutil.rmtree(self.repo_path(repository_path, repo_name)) + logger.info(f"Deleted repository: {repo_name} for user {username}") + return web.Response(text=f"Deleted repository {repo_name}") + except Exception as e: + logger.error(f"Error deleting repository {repo_name}: {str(e)}") + return web.Response(text=f"Error deleting repository: {str(e)}", status=500) + + @require_auth + async def clone_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 + host = request.host + clone_url = f"http://{host}/{repo_name}.git" + response_data = { + "repository": repo_name, + "clone_command": f"git clone {clone_url}", + "clone_url": clone_url + } + return web.json_response(response_data) + + @require_auth + async def push_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 + try: + data = await request.json() + except json.JSONDecodeError: + return web.Response(text="Invalid JSON data", status=400) + commit_message = data.get('commit_message', 'Update from server') + branch = data.get('branch', 'main') + changes = data.get('changes', []) + if not changes: + return web.Response(text="No changes provided", status=400) + with tempfile.TemporaryDirectory() as temp_dir: + temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir) + for change in changes: + file_path = os.path.join(temp_dir, change.get('file', '')) + content = change.get('content', '') + os.makedirs(os.path.dirname(file_path), exist_ok=True) + with open(file_path, 'w') as f: + f.write(content) + temp_repo.git.add(A=True) + if not temp_repo.config_reader().has_section('user'): + temp_repo.config_writer().set_value("user", "name", "Git Server").release() + temp_repo.config_writer().set_value("user", "email", "git@server.local").release() + temp_repo.index.commit(commit_message) + origin = temp_repo.remote('origin') + origin.push(refspec=f"{branch}:{branch}") + logger.info(f"Pushed to repository: {repo_name} for user {username}") + return web.Response(text=f"Successfully pushed changes to {repo_name}") + + @require_auth + async def pull_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 + try: + data = await request.json() + except json.JSONDecodeError: + data = {} + remote_url = data.get('remote_url') + branch = data.get('branch', 'main') + if not remote_url: + return web.Response(text="Remote URL is required", status=400) + with tempfile.TemporaryDirectory() as temp_dir: + try: + local_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir) + remote_name = "pull_source" + try: + remote = local_repo.create_remote(remote_name, remote_url) + except git.GitCommandError: + remote = local_repo.remote(remote_name) + remote.set_url(remote_url) + remote.fetch() + local_repo.git.merge(f"{remote_name}/{branch}") + origin = local_repo.remote('origin') + origin.push() + logger.info(f"Pulled to repository {repo_name} from {remote_url} for user {username}") + return web.Response(text=f"Successfully pulled changes from {remote_url} to {repo_name}") + except Exception as e: + logger.error(f"Error pulling to {repo_name}: {str(e)}") + 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) diff --git a/src/snek/system/cache.py b/src/snek/system/cache.py index 57e90a3..eed888a 100644 --- a/src/snek/system/cache.py +++ b/src/snek/system/cache.py @@ -1,67 +1,144 @@ -_C='delete' -_B='set' -_A='get' -import functools,json +import functools +import json + from snek.system import security -cache=functools.cache -CACHE_MAX_ITEMS_DEFAULT=5000 + +cache = functools.cache + +CACHE_MAX_ITEMS_DEFAULT = 5000 + + 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 - async def get(A,args): - B=args;await A.update_stat(B,_A) - try:A.lru.pop(A.lru.index(B)) - except:return - A.lru.insert(0,B) - while len(A.lru)>A.max_items:A.cache.pop(A.lru[-1]);A.lru.pop() - return A.cache[B] - async def get_stats(A): - C=[] - 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))}) - return C - 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 - async def update_stat(A,key,action): - C=action;B=key - if B not in A.stats:A.stats[B]={_B:0,_A:0,_C:0} - A.stats[B][C]=A.stats[B][C]+1 - def json_default(B,value): - A=value - try:return json.dumps(A.__dict__,default=str) - except:return str(A) - 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)) - async def set(A,args,result): - B=args;C=B not in A.cache;A.cache[B]=result;await A.update_stat(B,_B) - try:A.lru.pop(A.lru.index(B)) - except(ValueError,IndexError):pass - A.lru.insert(0,B) - while len(A.lru)>A.max_items:A.cache.pop(A.lru[-1]);A.lru.pop() - if C:A.version+=1 - async def delete(A,args): - B=args;await A.update_stat(B,_C) - if B in A.cache: - try:A.lru.pop(A.lru.index(B)) - except IndexError:pass - del A.cache[B] - def async_cache(A,func): - @functools.wraps(func) - async def B(*B,**C): - D=await A.create_cache_key(B,C);E=await A.get(D) - if E:return E - F=await func(*B,**C);await A.set(D,F);return F - return B - def async_delete_cache(A,func): - @functools.wraps(func) - async def B(*C,**D): - B=await A.create_cache_key(C,D) - if B in A.cache: - try:A.lru.pop(A.lru.index(B)) - except IndexError:pass - del A.cache[B] - return await func(*C,**D) - return B + def __init__(self, app, max_items=CACHE_MAX_ITEMS_DEFAULT): + self.app = app + self.cache = {} + self.max_items = max_items + self.stats = {} + self.lru = [] + self.version = ((42 + 420 + 1984 + 1990 + 10 + 6 + 71 + 3004 + 7245) ^ 1337) + 4 + + async def get(self, args): + await self.update_stat(args, "get") + try: + self.lru.pop(self.lru.index(args)) + except: + # print("Cache miss!", args, flush=True) + return None + self.lru.insert(0, args) + while len(self.lru) > self.max_items: + self.cache.pop(self.lru[-1]) + self.lru.pop() + # print("Cache hit!", args, flush=True) + return self.cache[args] + + async def get_stats(self): + all_ = [] + for key in self.lru: + all_.append( + { + "key": key, + "set": self.stats[key]["set"], + "get": self.stats[key]["get"], + "delete": self.stats[key]["delete"], + "value": str(self.serialize(self.cache[key].record)), + } + ) + return all_ + + def serialize(self, obj): + cpy = obj.copy() + cpy.pop("created_at", None) + cpy.pop("deleted_at", None) + cpy.pop("email", None) + cpy.pop("password", None) + return cpy + + async def update_stat(self, key, action): + if key not in self.stats: + self.stats[key] = {"set": 0, "get": 0, "delete": 0} + self.stats[key][action] = self.stats[key][action] + 1 + + 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): + 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): + 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): - B={} - @functools.wraps(func) - async def A(*A): - if A in B:return B[A] - C=await func(*A);B[A]=C;return C - return A \ No newline at end of file + cache = {} + + @functools.wraps(func) + async def wrapper(*args): + if args in cache: + return cache[args] + result = await func(*args) + cache[args] = result + return result + + return wrapper diff --git a/src/snek/system/form.py b/src/snek/system/form.py index 0ec782b..f4cf2d3 100644 --- a/src/snek/system/form.py +++ b/src/snek/system/form.py @@ -1,32 +1,120 @@ -_B='fields' -_A=None +# Written by retoor@molodetz.nl + +# This code defines a framework for handling HTML elements as Python objects, including specific classes for HTML, form input, and form button elements. It offers methods to convert these elements to JSON, manipulate them, and validate form data. + +# 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 + + 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) - 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 -class FormElement(HTMLElement):0 + def __init__( + self, + 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): - def __init__(A,type='text',place_holder=_A,*B,**C):super().__init__(*B,tag='input',**C);A.place_holder=place_holder;A.type=type - async def to_json(B):A=await super().to_json();A['place_holder']=B.place_holder;A['type']=B.type;return A + def __init__(self, type="text", place_holder=None, *args, **kwargs): + 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): - 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): - @property - def html_elements(self):return[A for A in self.fields if isinstance(A,HTMLElement)] - def set_user_data(A,data):return super().set_user_data(data.get(_B)) - async def to_json(D,encode=False): - B='is_valid';E=await super().to_json();C={} - for A in E.keys(): - if A==B:continue - F=getattr(D,A) - if isinstance(F,HTMLElement): - try:C[A]=E[A] - except KeyError:pass - G=all(A[B]for A in C.values());return{_B:C,B:G,'errors':await D.errors} - @property - async def errors(self): - A=[] - for B in self.html_elements:A+=await B.errors - return A - @property - async def is_valid(self):return False \ No newline at end of file + @property + def html_elements(self): + return [element for element in self.fields if isinstance(element, HTMLElement)] + + def set_user_data(self, data): + return super().set_user_data(data.get("fields")) + + async def to_json(self, encode=False): + elements = await super().to_json() + html_elements = {} + for element in elements.keys(): + if element == "is_valid": + # is_valid is async get property so we can't do getattr on it + continue + field = getattr(self, element) + if isinstance(field, HTMLElement): + try: + html_elements[element] = elements[element] + 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 diff --git a/src/snek/system/http.py b/src/snek/system/http.py index fa993d5..a1e87a4 100644 --- a/src/snek/system/http.py +++ b/src/snek/system/http.py @@ -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 -import aiohttp,imgkit + +import aiohttp +import imgkit from app.cache import time_cache_async from bs4 import BeautifulSoup + + async def crc32(data): - A=data - try:A=A.encode() - except:pass - return'crc32'+str(zlib.crc32(A)) -async def get_file(name,suffix='.cache'): - 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 public_touch(name=None):A=pathlib.Path('.').joinpath(str(uuid.uuid4())+name);A.open('wb').close();return A + try: + data = data.encode() + except: + pass + return "crc32" + str(zlib.crc32(data)) + + +async def get_file(name, suffix=".cache"): + 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): - A=url;C=asyncio.get_event_loop() - if not A.startswith('https'):A='https://'+A - B=await get_file('site-screenshot-'+A,'.png') - if B.exists():return B - B.touch() - def D():imgkit.from_url(A,B.absolute());return B - return await C.run_in_executor(None,D) -async def repair_links(base_url,html_content): - D='http';E=base_url;B='src';C='href';F=BeautifulSoup(html_content,'html.parser') - for A in F.find_all(['a','img','link']): - if A.has_attr(C)and not A[C].startswith(D):A[C]=urljoin(E,A[C]) - if A.has_attr(B)and not A[B].startswith(D):A[B]=urljoin(E,A[B]) - return F.prettify() -async def is_html_content(content): - B=False;A=content - if not A:return B - try:A=A.decode(errors='ignore') - except:pass - C=[' BaseModel: + if uid: + kwargs["uid"] = uid + record = 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 self.table.exists(**kwargs) + + async def count(self, **kwargs) -> int: + return 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 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 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 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 self.table.delete(**kwargs) diff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py index b530fb8..82a222e 100644 --- a/src/snek/system/markdown.py +++ b/src/snek/system/markdown.py @@ -1,35 +1,87 @@ -_A=True +# Original source: https://brandonjay.dev/posts/2021/render-markdown-html-in-python-with-jinja2 + from types import SimpleNamespace + from app.cache import time_cache_async -from mistune import HTMLRenderer,Markdown +from mistune import HTMLRenderer, Markdown from pygments import highlight from pygments.formatters import html from pygments.lexers import get_lexer_by_name + + 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() - def _escape(A,str):return str - def get_lexer(A,lang,default='bash'): - try:return get_lexer_by_name(lang,stripall=_A) - except:return get_lexer_by_name(default,stripall=_A) - def block_code(B,code,lang=None,info=None): - A=lang - if not A:A=info - if not A:A='bash' - C=B.get_lexer(A);D=html.HtmlFormatter(lineseparator='
');E=highlight(code,C,D);return E - 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) -def render_markdown_sync(app,markdown_string):A=MarkdownRenderer(app,None);B=Markdown(renderer=A);return B(markdown_string) + + _allow_harmful_protocols = True + + def __init__(self, app, template): + self.template = template + + self.app = app + self.env = self.app.jinja2_env + formatter = html.HtmlFormatter() + self.env.globals["highlight_styles"] = formatter.get_style_defs() + + 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="
") + 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 render_markdown_sync(app, markdown_string): + renderer = MarkdownRenderer(app, None) + markdown = Markdown(renderer=renderer) + return markdown(markdown_string) + + @time_cache_async(120) -async def render_markdown(app,markdown_string):return render_markdown_sync(app,markdown_string) -from jinja2 import TemplateSyntaxError,nodes +async def render_markdown(app, markdown_string): + return render_markdown_sync(app, markdown_string) + + +from jinja2 import TemplateSyntaxError, nodes from jinja2.ext import Extension from jinja2.nodes import Const + + +# Source: https://ron.sh/how-to-write-a-jinja2-extension/ class MarkdownExtension(Extension): - tags={'markdown'} - def __init__(A,environment):B=environment;A.app=SimpleNamespace(jinja2_env=B);super(MarkdownExtension,A).__init__(B) - def parse(D,parser): - A=parser;E=next(A.stream).lineno;B=[Const('')];C='' - try:B=[A.parse_expression()] - 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 _to_html(A,md_file,caller):return render_markdown_sync(A.app,caller()) \ No newline at end of file + tags = {"markdown"} + + def __init__(self, environment): + self.app = SimpleNamespace(jinja2_env=environment) + super(MarkdownExtension, self).__init__(environment) + + def parse(self, parser): + 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()) diff --git a/src/snek/system/middleware.py b/src/snek/system/middleware.py index 1437a3f..3a9a055 100644 --- a/src/snek/system/middleware.py +++ b/src/snek/system/middleware.py @@ -1,21 +1,53 @@ -_D='Access-Control-Allow-Credentials' -_C='Access-Control-Allow-Headers' -_B='Access-Control-Allow-Methods' -_A='Access-Control-Allow-Origin' +# Written by retoor@molodetz.nl + +# This code provides middleware functions for an aiohttp server to manage and modify CORS (Cross-Origin Resource Sharing) headers. + +# 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 + + @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 -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 -async def auth_middleware(request,handler): - B='uid';C='user';A=request;A[C]=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)) - return await handler(A) +async def auth_middleware(request, handler): + request["user"] = None + if request.session.get("uid") and request.session.get("logged_in"): + request["user"] = await request.app.services.user.get( + uid=request.app.session.get("uid") + ) + return await handler(request) + + @web.middleware -async def cors_middleware(request,handler): - C='Allow';D=handler;B=request - if B.headers.get(C):return await D(B) - A=await D(B) - if B.headers.get(C):return A - A.headers[_A]='*';A.headers[_B]='GET, POST, PUT, DELETE, OPTIONS';A.headers[_C]='*';A.headers[_D]='true';return A \ No newline at end of file +async def cors_middleware(request, handler): + if request.headers.get("Allow"): + return await handler(request) + + response = await handler(request) + 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 diff --git a/src/snek/system/model.py b/src/snek/system/model.py index b036329..9e9830d 100644 --- a/src/snek/system/model.py +++ b/src/snek/system/model.py @@ -1,139 +1,377 @@ -_I='deleted_at' -_H='updated_at' -_G='created_at' -_F='is_valid' -_E='name' -_D=False -_C='value' -_B=True -_A=None -import copy,json,re,uuid +# Written by retoor@molodetz.nl + +# The script defines a flexible validation and field management system for models, with capabilities for setting attributes, validation, error handling, and JSON conversion. It includes classes for managing various field types with specific properties such as UUID, timestamps for creation and updates, and custom validation rules. + +# This script utilizes external Python libraries such as 're' for regex operations, 'uuid' for generating unique identifiers, and 'json' for data interchange. The 'datetime' and 'timezone' modules from the Python standard library are used for date and time operations. 'OrderedDict' from 'collections' provides enhanced dictionary capabilities, and 'copy' allows deep copying of objects. + +# 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. + + +import copy +import json +import re +import uuid from collections import OrderedDict -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)) -def add_attrs(**A): - def B(func): - for(B,C)in A.items():setattr(func,B,C) - return func - 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) +from datetime import datetime, timezone + +TIMESTAMP_REGEX = r"^\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)) + + +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: - _index=0 - @property - def value(self):return self._value - @value.setter - def value(self,val):self._value=json.loads(json.dumps(val,default=str)) - @property - def initial_value(self):return self.value - def custom_validation(A):return _B - 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) - @property - async def errors(self): - A=self;B=[] - if A.value is _A and A.required:B.append('Field is required.');return B - if A.value is _A:return B - if A.kind in[int,float]: - if A.min_num is not _A and A.valueA.max_num:B.append(f"Field should be maximal {A.max_num}.") - if A.min_length is not _A and len(A.value)A.max_length:B.append(f"Field should be maximal {A.max_length} characters long.") - if A.regex and A.value and not re.match(A.regex,A.value):B.append('Invalid value.') - if A.kind and not isinstance(A.value,A.kind):B.append(f"Invalid kind. It is supposed to be {A.kind}.") - return B - async def validate(B): - A=await B.errors - if A:raise ValueError(f"Errors: {A}.") - return _B - def __repr__(A):return str(A.to_json()) - @property - async def is_valid(self): - try:await self.validate();return _B - except ValueError:return _D - 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} + _index = 0 + + @property + def value(self): + return self._value + + @value.setter + def value(self, val): + self._value = json.loads(json.dumps(val, default=str)) + + @property + def initial_value(self): + return self.value + + def custom_validation(self): + return True + + def __init__( + self, + required=False, + min_num=None, + max_num=None, + min_length=None, + max_length=None, + regex=None, + value=None, + kind=None, + help_text=None, + app=None, + model=None, + **kwargs, + ): + 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): - index=1 - def __init__(A,name=_A,save=_B,*B,**C):A.name=name;A.save=save;super().__init__(*B,**C) - async def to_json(B):A=await super().to_json();A[_E]=B.name;return A + + index = 1 + + 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): - @property - def initial_value(self):return now() - def update(A): - if not A.value:A.value=now() + + @property + def initial_value(self): + return now() + + def update(self): + if not self.value: + self.value = now() + + class UpdatedField(ModelField): - def update(A):A.value=now() + + def update(self): + self.value = now() + + class DeletedField(ModelField): - def update(A):A.value=now() + + def update(self): + self.value = now() + + class UUIDField(ModelField): - @property - def value(self):return str(self._value) - @value.setter - def value(self,val):self._value=str(val) - @property - def initial_value(self):return str(uuid.uuid4()) + + @property + def value(self): + return str(self._value) + + @value.setter + def value(self, val): + self._value = str(val) + + @property + def initial_value(self): + return str(uuid.uuid4()) + + 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 - async def from_record(B,record,mapper):A=B();A.mapper=mapper;A.record=record;return A - @property - def mapper(self):return self._mapper - @mapper.setter - def mapper(self,value):self._mapper=value - @property - def record(self):return{A:B.value for(A,B)in self.fields.items()} - @record.setter - def record(self,val): - A=self - for(B,C)in val.items(): - D=A.fields.get(B) - if not D:continue - A[B]=C - return A - def __init__(A,*F,**C): - D='app';A._mapper=C.get('mapper');A.app=C.get(D);A.fields={} - for B in dir(A.__class__): - E=getattr(A.__class__,B) - 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): - A=B.__dict__.get(key) - if isinstance(A,Validator):A.value=value - def __getattr__(B,key): - A=B.__dict__.get(key) - if isinstance(A,Validator):return A.value - return A - def set_user_data(C,data): - for(D,A)in data.items(): - B=C.fields.get(D) - if not B:continue - if A.get(_E):A=A.get(_C) - B.value=A - @property - async def is_valid(self):return all([await A.is_valid for A in self.fields.values()]) - def __getitem__(B,key): - A=B.__dict__.get(key) - if isinstance(A,Validator):return A.value - def __setattr__(A,key,value): - B=value;C=getattr(A,key) - if isinstance(C,Validator):C.value=B - else:A.__dict__[key]=B - @property - async def recordz(self): - D=await self.to_json();B={} - for(C,A)in D.items(): - if not isinstance(A,dict)or _C not in A:continue - if getattr(self,C).save:B[C]=A.get(_C) - return B - async def to_json(A,encode=_D): - 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}) - for(C,D)in A.fields.items(): - if C=='record':continue - D=A.__dict__[C] - if hasattr(D,_C):B[C]=await D.to_json() - if encode:return json.dumps(B,indent=2) - return B + + uid = UUIDField(name="uid", required=True) + created_at = CreatedField( + name="created_at", + required=True, + regex=TIMESTAMP_REGEX, + place_holder="Created at", + ) + updated_at = UpdatedField( + name="updated_at", regex=TIMESTAMP_REGEX, place_holder="Updated at" + ) + deleted_at = DeletedField( + name="deleted_at", regex=TIMESTAMP_REGEX, place_holder="Deleted at" + ) + + @classmethod + async def from_record(cls, record, mapper): + model = cls() + model.mapper = mapper + model.record = record + return model + + @property + def mapper(self): + return self._mapper + + @mapper.setter + def mapper(self, value): + self._mapper = value + + @property + def record(self): + return {key: field.value for key, field in self.fields.items()} + + @record.setter + def record(self, val): + for key, value in val.items(): + field = self.fields.get(key) + if not field: + continue + self[key] = value + return self + + def __init__(self, *args, **kwargs): + self._mapper = kwargs.get("mapper") + self.app = kwargs.get("app") + self.fields = {} + for key in dir(self.__class__): + obj = getattr(self.__class__, key) + + if isinstance(obj, Validator): + self.__dict__[key] = copy.deepcopy(obj) + self.__dict__[key].value = kwargs.pop( + key, self.__dict__[key].initial_value + ) + self.fields[key] = self.__dict__[key] + self.fields[key].model = self + self.fields[key].app = kwargs.get("app") + + 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): - 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): - 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 \ No newline at end of file + + 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 diff --git a/src/snek/system/object.py b/src/snek/system/object.py index a36bb76..f91ec42 100644 --- a/src/snek/system/object.py +++ b/src/snek/system/object.py @@ -1,7 +1,13 @@ class Object: - def __init__(A,*C,**D): - for B in C: - if isinstance(B,dict):A.__dict__.update(B) - A.__dict__.update(D) - def __getitem__(A,key):return A.__dict__[key] - def __setitem__(A,key,value):A.__dict__[key]=value \ No newline at end of file + + def __init__(self, *args, **kwargs): + for arg in args: + if isinstance(arg, dict): + self.__dict__.update(arg) + self.__dict__.update(kwargs) + + def __getitem__(self, key): + return self.__dict__[key] + + def __setitem__(self, key, value): + self.__dict__[key] = value diff --git a/src/snek/system/profiler.py b/src/snek/system/profiler.py index d196b30..e0e5542 100644 --- a/src/snek/system/profiler.py +++ b/src/snek/system/profiler.py @@ -1,17 +1,46 @@ -import cProfile,pstats,sys +import cProfile +import pstats +import sys + from aiohttp import web -profiler=None + +profiler = None import io + + @web.middleware -async def profile_middleware(request,handler): - global profiler - if not profiler:profiler=cProfile.Profile() - profiler.enable();B=await handler(request);profiler.disable();A=pstats.Stats(profiler,stream=sys.stdout);A.sort_stats('cumulative');A.print_stats();return B -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()) +async def profile_middleware(request, handler): + global profiler + if not profiler: + profiler = cProfile.Profile() + 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: - def __init__(A): - global profiler - if profiler is None:profiler=cProfile.Profile() - A.profiler=profiler - async def __aenter__(A):A.profiler.enable() - async def __aexit__(A,*B,**C):A.profiler.disable() \ No newline at end of file + + def __init__(self): + global profiler + if profiler is None: + profiler = cProfile.Profile() + self.profiler = profiler + + async def __aenter__(self): + self.profiler.enable() + + async def __aexit__(self, *args, **kwargs): + self.profiler.disable() diff --git a/src/snek/system/security.py b/src/snek/system/security.py index 8d5ced9..43b61fe 100644 --- a/src/snek/system/security.py +++ b/src/snek/system/security.py @@ -1,24 +1,77 @@ -_A='snekker-de-snek-' -import hashlib,uuid -DEFAULT_SALT=_A -DEFAULT_NS=_A +import hashlib +import uuid + +DEFAULT_SALT = "snekker-de-snek-" +DEFAULT_NS = "snekker-de-snek-" + + class UIDNS: - def __init__(A,name):'Initialize UIDNS with a name.';A.name=name - @property - def bytes(self):'Return the bytes representation of the name.';return self.name.encode() -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 - try:ns=ns.decode() - except AttributeError:pass - if not A:A=str(uuid.uuid4()) - try:A=A.decode() - except AttributeError:pass - return str(uuid.uuid5(UIDNS(ns),A)) -async def hash(data,salt=DEFAULT_SALT): - '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) - except AttributeError:pass - try:A=A.encode(errors=C) - except AttributeError:pass - D=A+B;E=hashlib.sha256(D);return E.hexdigest() -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 \ No newline at end of file + def __init__(self, name: str) -> None: + """Initialize UIDNS with a name.""" + self.name = name + + @property + def bytes(self) -> bytes: + """Return the bytes representation of the name.""" + return self.name.encode() + + +def uid(value: str = None, ns: str = DEFAULT_NS) -> str: + """Generate a UUID based on the provided value and namespace. + + Args: + value (str): The value to generate the UUID from. If None, a new UUID is created. + ns (str): The namespace to use for UUID generation. + + Returns: + 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)) + + +async def hash(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 verify(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 await hash(string) == hashed diff --git a/src/snek/system/service.py b/src/snek/system/service.py index eb735b1..c6d2afc 100644 --- a/src/snek/system/service.py +++ b/src/snek/system/service.py @@ -1,42 +1,67 @@ -_B='uid' -_A=None from snek.mapper import get_mapper from snek.model.user import UserModel from snek.system.mapper import BaseMapper + + class BaseService: - mapper_name:BaseMapper=_A - @property - def services(self):return self.app.services - def __init__(A,app): - A.app=app;A.cache=app.cache - if A.mapper_name:A.mapper=get_mapper(A.mapper_name,app=A.app) - else:A.mapper=_A - async def exists(C,uid=_A,**A): - B=uid - if B: - if not A and await C.cache.get(B):return True - A[_B]=B - return await C.count(**A)>0 - async def count(A,**B):return await A.mapper.count(**B) - async def new(A,**B):return await A.mapper.new() - async def query(A,sql,*B): - for C in A.app.db.query(sql,*B):yield C - async def get(B,uid=_A,**C): - D=uid - if D: - if not C: - A=await B.cache.get(D) - if False and A and A.__class__==B.mapper.model_class:return A - C[_B]=D - A=await B.mapper.get(**C) - if A:await B.cache.set(A[_B],A) - return A - async def save(B,model): - A=model - if await B.mapper.save(A):await B.cache.set(A[_B],A);return True - C=await A.errors;raise Exception(f"Couldn't save model. Errors: f{C}") - async def find(C,**A): - B='_limit' - if B not in A or int(A.get(B))>30:A[B]=60 - async for D in C.mapper.find(**A):yield D - async def delete(A,**B):return await A.mapper.delete(**B) \ No newline at end of file + + mapper_name: BaseMapper = None + + @property + def services(self): + return self.app.services + + def __init__(self, app): + self.app = app + self.cache = app.cache + if self.mapper_name: + self.mapper = get_mapper(self.mapper_name, app=self.app) + else: + self.mapper = None + + async def exists(self, uid=None, **kwargs): + if uid: + if not kwargs and await self.cache.get(uid): + return True + kwargs["uid"] = uid + return await self.count(**kwargs) > 0 + + async def count(self, **kwargs): + return await self.mapper.count(**kwargs) + + async def new(self, **kwargs): + return await self.mapper.new() + + async def query(self, sql, *args): + for record in self.app.db.query(sql, *args): + yield record + + async def get(self, uid=None, **kwargs): + if uid: + if not kwargs: + 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) diff --git a/src/snek/system/template.py b/src/snek/system/template.py index 1630219..d4b6819 100644 --- a/src/snek/system/template.py +++ b/src/snek/system/template.py @@ -1,82 +1,249 @@ -_G=':snek1:' -_F='status' -_E='_to_html' -_D='alias' -_C=True -_B='html.parser' -_A='href' import re from types import SimpleNamespace + import emoji from bs4 import BeautifulSoup -from jinja2 import TemplateSyntaxError,nodes +from jinja2 import TemplateSyntaxError, nodes from jinja2.ext import Extension from jinja2.nodes import Const -emoji.EMOJI_DATA['']={'en':_G,_F:2,'E':.6,_D:[_G]} -emoji.EMOJI_DATA['⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣠⣤⡴⠶⢶⣞⣛⣛⡳⣳⠶⣶⡶⢶⢶⣦⣤⣄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⠶⠚⣫⣴⣬⠐⣶⣿⣿⣏⣽⣿⣿⣇⢿⣯⣿⣿⣻⣿⣿⣾⣮⣹⣿⢶⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⢾⣻⣽⣾⡇⢡⣿⣿⣇⡟⣿⣿⣿⣼⣿⣿⣿⣿⢸⣿⣟⣿⣷⢻⣿⣿⣿⣷⡽⣧⣹⡻⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠟⣫⣶⣿⣟⣾⡿⠀⣾⣿⣿⢹⢿⣿⣿⣏⣿⣿⣿⣿⣿⣺⣿⡏⣿⣿⣏⣿⣿⣿⣿⣿⣞⣿⣿⣯⡻⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⣠⡴⣛⣵⣻⣟⣿⢯⣿⢟⡆⣀⣿⣿⣿⣿⣾⣿⣿⢹⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣸⣿⣿⣿⣿⣿⡾⣿⣻⣿⣮⠻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⡀⠀⠀⠀⢀⣠⡾⣻⣾⡿⣳⡟⣾⢿⣿⢯⣿⡇⢸⣿⣿⣿⣇⣿⣿⡿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⢻⣏⣿⣿⡵⣝⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣟⢧⣤⣶⢾⣻⣿⣾⣿⣿⢳⣿⢻⣏⣿⢿⣿⡿⠀⣾⣿⣿⣿⢹⣿⣿⣗⣿⣿⣿⣿⣿⢏⣿⣿⠋⣿⣿⡟⡈⣿⣿⣿⠸⣿⣿⣹⣿⢸⣿⣧⢻⣮⢿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠿⠿⠿⢿⡟⣿⣿⣿⢧⣿⣟⡟⣾⣯⣿⡿⣽⡇⣿⣿⣿⣿⣸⣿⣿⠹⣿⡿⢻⡿⢣⣿⡿⣳⣖⣿⣿⢱⣿⢹⣿⣿⠀⣿⣿⠘⣿⡞⣿⣿⡸⣿⣯⢿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣼⣿⣿⡿⣼⣿⣿⢧⣿⣼⣿⣻⣿⡃⣿⣿⣿⣿⣿⣿⣿⣀⣿⢧⠘⣣⢸⡿⣻⣿⣿⡘⠟⣿⣿⡘⠿⠟⣟⣻⡿⣀⢿⡇⢻⣿⡇⣿⣿⢸⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣏⣿⣿⣿⢳⣿⣿⣿⣸⢷⣿⢷⣿⢟⢨⣿⣿⣿⣿⣿⣿⣯⣿⣦⠖⠘⡛⣡⠬⠭⠭⠛⠂⠰⣿⣿⡇⠀⣰⡏⢛⣴⣿⡏⢸⣼⡿⠇⣿⣿⣧⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣹⣿⣿⡏⣿⣿⣿⡇⣿⣾⣯⣿⣯⡿⢸⣿⣿⣿⣿⣿⢿⡃⣽⣶⣿⣿⣿⣿⣿⣿⣿⣷⣤⣗⠊⣻⣿⣶⣿⣶⣿⣿⣿⡇⣾⠏⡁⠀⣿⣿⣧⣹⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣧⣿⣿⣿⣹⣿⣿⣿⡇⣿⡿⣾⠳⠋⣄⣼⣿⣿⣿⣻⣾⣿⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣫⣤⣄⣀⣿⠇⣿⣿⣿⣻⡌⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣸⣿⣿⣏⣿⣿⣿⣿⣿⣿⣷⣇⢀⣾⡋⣽⣿⣿⣿⣿⣿⣿⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣱⣿⣿⣿⣿⣿⡇⣬⣻⣿⣽⡇⠸⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣇⣿⣿⣿⣹⣿⣿⢏⣅⢀⠲⣤⣙⠼⢿⡇⣿⣿⣿⣿⣿⡿⠿⠻⠟⠻⠿⠿⣷⣽⡻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣵⣿⣿⣿⣿⣿⣿⡇⣿⣧⣿⢾⡇⠀⢿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⢸⣿⣿⡿⣾⣿⣏⣾⠇⠈⠀⠈⢻⣿⣷⡁⣿⣿⣿⣿⣿⣧⣤⣴⣶⠀⠀⠀⠀⣊⡙⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⣻⣿⡇⣿⣿⣿⣻⡇⠀⢸⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡏⣿⣿⣿⡗⣿⣿⢸⡟⣼⣀⠏⠁⡀⢿⣿⡇⣹⣿⣿⣿⣿⣿⡇⢿⣯⠀⡄⠀⠁⠉⠙⣿⣮⡝⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠁⠀⣀⣀⡀⠁⢿⣻⣿⣿⡇⠀⠈⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢣⣿⣿⣿⢸⣿⣏⢾⡧⡿⠋⠉⢁⣡⣿⣿⣧⣿⣿⣿⣿⣿⣿⣃⣷⣝⣀⣽⣤⣧⣤⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⣀⠀⠀⠉⢩⣿⡇⣶⣳⣿⣽⡇⠀⢠⡿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣸⣿⣿⡿⣼⣿⣷⠸⠃⢻⡜⢿⣿⢟⣿⣿⣿⣿⣿⣿⣿⣿⣿⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣿⣷⣥⣤⣥⣤⣿⣿⡅⣿⣿⣿⣿⡇⠀⣼⣿⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⣿⣿⣿⣿⣿⣿⣿⣄⢷⣄⣉⠮⠉⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⢼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⢹⣿⣿⣿⣿⣿⣿⣿⡇⣿⣿⣿⣿⡇⢼⣿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢧⣿⣿⣿⣿⣿⣿⣿⣿⣮⢿⣿⣿⣷⣦⡈⢿⣿⣿⣿⣿⣿⣿⣿⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢇⣿⣿⣿⣿⣿⣿⣿⣇⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣵⡝⢿⣿⣿⣿⣶⣍⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢉⣾⣿⣿⣿⣿⣿⣿⣿⢹⣿⣿⣿⢿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⡟⣾⣿⣿⣿⣿⣿⣿⣿⣿⡟⣿⢻⢳⢸⢩⣟⣛⠓⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣿⣿⣿⣿⣿⣿⣿⡿⢸⣿⣿⣿⣼⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⣿⣿⣿⣿⣿⠇⣿⣿⣿⠇⣿⢸⢸⣸⡇⢸⣿⠇⣿⣿⣿⣿⣿⣿⢹⣿⣯⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⢿⢿⠛⢻⣿⣿⣿⣿⣿⣿⣿⠃⢸⣿⣿⣿⢾⡃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣷⣿⣿⣿⣿⣿⠀⣿⣻⣿⢹⣿⣸⠘⡿⡇⢸⣿⡄⢿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢛⣵⣶⣶⣬⠀⢠⣿⣿⣿⣿⣿⣿⣿⠏⢄⣸⣿⣿⣿⣿⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⢸⣿⣿⣿⣿⡟⢸⡿⣿⣿⢸⡇⡖⡄⠇⣇⢸⣿⡓⢻⣿⣿⣿⣿⣿⢺⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⣿⣿⣿⣿⢧⣿⣿⣿⣿⣿⣿⣿⠋⢨⠀⣿⣿⣿⣏⣿⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡏⣾⣿⣿⣿⣿⠃⣼⣷⣿⡏⣿⣿⠇⢹⢸⣿⢺⣿⣿⢸⣿⣿⣿⣿⣿⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⣙⡛⣛⢡⣾⣿⠻⢿⣿⣿⣟⣱⡆⠨⠀⣿⣿⢿⡗⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣹⣿⣿⣿⣿⣿⠀⣼⣹⣿⢣⣿⣿⢠⣸⣼⣿⡞⣿⣿⣾⣿⣿⣿⣿⣿⢸⣦⣟⢿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣷⣾⢿⣿⣿⠈⢷⡝⢻⡊⠁⣧⠀⠃⣿⣿⡆⢋⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢯⣿⣿⣿⣿⣿⣿⡄⣿⣿⣿⣸⣿⣿⣸⢻⣿⣿⡗⣿⣿⡏⣿⣿⣿⣿⣿⠈⣿⣿⣬⣙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⡆⢨⣁⣘⣷⣄⣿⠀⢠⣿⣿⣣⣈⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣟⣿⣿⣿⣿⣿⣿⣿⢀⠯⣿⡏⣿⣿⡇⢻⣽⣿⣿⣟⣻⣿⡇⣿⣿⡏⣿⣿⣰⡼⢺⡾⣏⣿⣨⡟⠿⣿⣿⣿⣿⣿⣿⣿⢟⡉⡆⡎⠿⣿⣿⣮⡻⡇⢸⣿⣏⢿⣿⣞⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣟⣾⣿⣿⢇⣾⣿⣿⢋⣠⢰⣿⢿⣿⣿⢰⢸⢹⣿⣿⣿⣸⣿⣧⣿⣿⡇⣿⣿⢼⣿⣦⡹⢹⡜⣇⢿⣹⣶⣍⡛⠻⠿⣫⡖⡟⣠⣱⣧⣿⣞⣻⣿⣿⣦⡸⣿⣿⡜⣿⣿⣯⢿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢟⣾⣿⣿⢏⣾⣿⣿⡏⣾⡏⢸⣿⣾⣿⡇⠆⣿⢸⣿⣿⣿⡏⣿⣷⢹⣿⣷⣿⣿⢸⣿⣿⣿⣦⡻⡸⡞⣧⢿⠧⣇⠙⠿⣿⣼⢣⠃⢋⠃⣿⠈⠳⣝⢿⣿⣿⣮⣝⢿⡼⣿⣿⣷⡩⣽⣻⢶⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⢯⣿⣿⡿⣳⣿⣿⣿⡿⢸⣿⠇⣺⣿⢯⣿⡝⣸⡿⡘⣽⣿⣿⣗⢿⣿⢸⣿⣿⣿⣿⠦⣿⣿⣿⣿⣿⣶⣭⣘⠎⠘⢣⣷⡆⠂⠥⠁⣂⡟⠟⢿⣀⠀⠉⠳⣝⢿⣿⣿⣿⣦⣙⣿⣿⣿⣎⣿⣷⣝⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡾⣵⣿⣿⣏⢼⣿⣽⣿⡿⣁⣿⡿⠸⠹⢫⣿⣟⣼⣿⠃⣵⣿⡿⢸⣿⠘⣿⠘⣿⡏⣿⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⢲⣸⡿⣳⣮⣝⡿⣿⣿⣿⣾⣿⣿⣿⣶⣿⣷⡎⠻⣿⣿⣿⣿⣷⣿⡻⢸⡿⣿⣿⣽⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢟⣼⡿⣯⡿⣯⣿⢳⣿⣿⠗⡋⠁⠀⠀⠈⠒⢦⣝⠻⣷⣿⡿⢟⡥⣼⣿⠀⠹⢀⣿⡿⣿⣿⡏⢹⣿⣿⣿⣿⣿⣿⣿⣿⠸⢺⡇⡇⣿⢸⣿⡆⣭⣛⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⡼⡇⣿⣿⣿⣯⢷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡾⣣⡿⢋⡾⠋⣼⢳⣿⢿⠟⠡⢈⣥⠶⠟⠛⠿⠷⣦⣽⣷⣤⡐⢾⣿⣷⣿⢏⣀⡀⠀⣿⣷⣿⣿⣿⢨⢿⣿⣿⣿⣿⣿⣿⣿⢸⠾⡧⣿⢿⣸⣿⣶⢸⣿⣿⡗⠶⢯⣭⣛⡻⡿⠿⢿⣿⣿⣿⣭⣝⣻⡇⣻⣿⣻⢿⣿⣧⠙⢷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣟⣽⢟⣵⠟⠀⢰⡿⣾⡿⢉⣶⡾⠋⠁⠀⠀⠀⠀⠀⠀⠉⠙⠿⣿⣦⠈⠉⠠⠀⠻⣿⠀⣿⣿⣿⣿⣿⣺⣟⣿⣿⣿⣿⣿⣿⡇⢸⣶⡟⢿⡼⠇⠿⣱⠘⣿⣧⣷⠀⠀⢀⣩⡇⠿⣟⣃⣬⣿⣿⣿⣿⣿⡇⣿⢸⣿⣷⣽⣿⣯⡨⠻⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢿⣾⣷⠟⠁⠀⠀⣾⢿⣿⠃⡞⣩⣶⣿⣿⣿⣿⣷⣦⡄⠀⠀⠀⠀⣠⣼⣷⡀⠀⢣⠀⠘⠃⣿⣿⣿⣿⣿⣏⣿⣞⣿⣿⣿⣿⣿⡇⠀⠈⠑⠾⢧⣾⡞⣵⣷⢻⣿⠻⣶⣟⣿⣽⣶⣿⣿⣿⣿⣿⣿⣿⣽⣿⡇⣿⠈⣿⣿⣿⣿⣻⣧⢱⣜⢿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⡿⠁⠀⠀⠀⢸⡟⣿⠇⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⣴⣿⣿⣿⣿⣄⠀⣃⠀⠀⣿⣿⢹⣿⣿⣇⢿⣿⣞⢿⣿⣿⣿⡇⢠⣤⣤⣄⡀⢤⣬⠁⠈⠁⣿⣾⣿⣿⡿⢿⣻⢛⣭⣿⢏⣼⣿⣿⣿⣿⠇⣮⣤⣿⣿⣿⣿⣿⣿⣇⢿⣎⢿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠋⠀⠀⠀⠀⢀⣿⢻⡟⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀⠀⣼⣿⣿⣿⣿⣿⣿⣦⠘⡇⡀⣿⣿⢸⣿⣿⣿⠸⣿⣿⣾⣿⣿⣿⡇⢰⣬⣝⡻⢿⣇⣂⣬⣤⣙⡋⠭⢥⣀⣀⠈⠙⠃⣫⣷⣿⣿⣿⣿⣭⣿⢢⣿⣿⣿⣿⣿⣿⣿⣿⣿⠈⣿⣎⢿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⡟⣾⢁⣾⣿⠉⠙⣿⣿⣿⣿⣿⣿⡿⠁⠀⣼⣿⣿⣿⣿⡿⠿⣿⣿⡇⢁⣿⢹⣿⣾⣿⣿⣿⡄⣜⢿⣿⣿⣿⣿⣿⠀⠻⣿⣿⣶⣍⠻⣿⣿⣿⣿⡿⢃⣾⠏⣠⣴⣿⣿⡿⠿⣛⢩⡜⢿⢏⢼⣿⣿⣿⣿⣿⣾⢿⣿⣿⠀⣿⢻⣎⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⢹⡟⣸⣿⡇⠀⠀⠈⢿⣿⣿⡿⠋⠀⠀⢠⣿⡿⠟⠉⠁⣠⡾⢋⣿⣧⠘⠻⢸⣿⣏⣿⣿⣿⣧⢉⣓⣈⢛⡿⠿⣿⣦⣶⣿⣿⣿⠿⣂⣤⡽⣿⠏⣴⡿⢋⣾⡿⠟⠋⠐⣾⣿⣿⡄⠻⣶⢿⣣⣮⣝⢿⣿⣿⣿⣷⡻⣿⠀⡟⠈⣿⡜⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⡏⣿⡇⣿⣿⠃⠀⠀⠀⠀⠙⠉⠀⠀⠀⠀⠘⠉⠀⣀⣴⡾⢋⣵⣿⣿⣧⡄⠀⢸⣿⣿⣿⣿⣿⣿⡄⠙⠛⢓⣙⢿⣷⣾⣿⣿⣿⣵⣾⣿⣿⣧⣄⣸⠋⢤⡬⠥⠀⠀⢶⡄⠈⠹⣿⣇⢀⠈⢝⣿⣿⣿⣿⡽⣿⣿⣿⡿⠁⢠⡇⠀⢻⣿⣹⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⡿⣸⣿⣷⠸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⣾⠟⣩⣶⣿⣿⣿⣿⣿⣿⠀⢸⣿⣿⣿⣿⣿⣿⣧⠸⠛⣛⣯⣤⣿⡻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣔⢶⣶⣷⡀⢻⣄⠀⠙⠿⣎⠀⠈⠻⣿⣿⣿⣿⣟⣿⢿⠁⠀⣼⠀⠀⠘⣿⣧⢻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⢣⣿⣿⣿⡇⠃⠀⠀⠀⠀⠀⠀⠀⣀⣤⣾⣿⡿⢛⣥⣾⣿⣿⣿⣿⣿⣿⣿⡟⠀⢸⣿⣿⣿⣿⣿⣿⣿⡄⢶⣭⣙⡛⠿⢿⣾⣝⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⡻⢿⣀⠙⠂⠀⣀⠈⠑⠀⠀⠀⢠⣮⠍⡉⢠⡌⠁⠀⠇⢀⣤⡀⢻⡟⢏⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣏⣿⣿⣿⣿⣠⠈⠀⠀⢀⣠⣴⣾⣿⣿⣿⠟⣋⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠁⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⡀⠉⡛⢿⣷⣦⣬⣉⠓⢮⡻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣝⠦⠀⠂⠙⣷⣄⡀⠀⠀⠀⠀⠀⠐⠀⠀⠀⣶⠀⣸⣿⡇⠸⣇⠘⡜⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡟⣼⣿⣿⣿⡏⣿⢰⣶⣿⠛⠛⠛⠉⠉⠁⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⢻⣿⣧⠀⢹⣷⣮⠛⣿⣿⣿⣦⡌⠐⡝⢻⣿⣿⣿⣶⣽⣿⣿⣿⣿⣿⣷⡄⠀⢠⢹⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⣼⠃⠀⠘⣿⠃⠀⣭⠀⢻⠙⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⢳⣿⣿⣿⣿⣷⣿⡆⣿⠃⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠀⢠⡇⢰⠂⣿⣿⣿⣿⣿⣿⡈⢿⣿⡄⠈⣿⣿⣷⣤⡉⠛⠿⣿⣷⣦⣔⡙⠻⣿⣿⣿⣿⣝⢿⣿⣿⣿⣿⣦⡀⢈⢻⣿⣿⣿⣄⢤⣀⠀⢀⣴⠏⠀⠀⠀⠀⠀⣾⣿⡇⢸⣧⢻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⢯⣿⣿⣿⣿⣿⣿⣿⡇⣇⢀⣀⣤⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⣡⠄⠀⣼⡇⢺⠁⣿⣿⣧⣿⣿⣿⡇⠈⢿⣿⡀⢹⣿⣿⣿⣿⣯⣻⣶⣯⣽⣿⠿⣿⣾⣽⣻⢿⣿⣿⣽⣿⢿⣿⣿⣿⣞⣇⢙⢿⣿⣿⣯⣻⣿⣶⣤⣀⠀⠀⠀⠀⠀⢻⡿⠁⠈⣿⣧⢻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣏⣿⣿⣿⣿⢿⣿⣿⡿⢃⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠁⢹⣿⣿⣿⣿⡟⠀⣸⣿⡇⠈⠀⣿⣿⣿⣿⡏⣿⣧⠀⠀⠻⣷⡜⣿⣿⣿⣿⣿⣿⣷⣽⡻⣿⣿⣷⣿⣽⣛⡿⢾⣽⣛⢿⣿⣾⣝⡿⣿⣯⡃⢣⡻⣿⣿⣷⡽⣿⣿⣿⣷⣦⣀⠀⠀⠀⠀⠀⠀⢿⣿⣧⢻⡄⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣟⣿⣿⢯⣿⢏⣾⣿⣿⢃⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠀⣠⣿⣿⣿⣿⣿⠀⣸⣿⣿⡇⠀⠀⣿⣿⢹⣿⡇⢻⣿⠀⠀⢠⡌⠿⣜⢿⣿⣿⣿⣿⣿⣿⣿⣾⣝⡿⣿⣿⣿⣿⣿⣶⣯⣝⣊⠽⢛⠿⣷⣝⢷⣤⠳⡜⢿⣿⣿⣝⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⢸⣿⣿⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⡟⣾⣿⣯⣿⢣⣾⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⣠⣼⣿⣿⣿⣿⣿⠃⣰⣿⣿⣿⡇⡄⠀⣿⣿⢸⣿⡇⠈⢿⡇⠀⣿⠇⣷⣯⣃⣙⣛⣛⣵⣿⣿⣿⣿⣿⣿⣾⣝⡻⣿⣿⣿⣿⣿⣿⣿⣷⣮⣄⡈⠳⢜⠃⠘⢎⢿⣿⣿⣞⢿⣿⣿⣿⣿⣿⣷⣆⡀⠀⠘⣿⡟⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣟⣽⣿⣯⣿⣣⣿⣿⣿⣿⣿⡄⢿⣿⣿⣿⣿⣿⣿⡿⠟⣡⣾⣿⣿⣿⣿⣿⣿⡏⣰⣿⣿⡿⢹⡇⣾⠀⣿⣿⣾⣿⡇⠀⠈⢷⢰⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣽⣻⢿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣦⣀⡀⠈⠛⠻⢿⣎⢿⣿⣿⣿⣿⣿⣿⣷⣄⠀⣿⡇⠘⣧⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⢟⣾⣿⣻⡿⣵⣿⣿⣿⣿⣟⣾⣯⠌⠛⠛⠛⠛⠉⠁⢠⣾⣿⣿⣿⣿⣿⣿⡿⠏⣰⣿⣿⡿⠁⢸⡇⢿⠀⣿⣿⣿⣿⡇⠀⠠⠈⠂⢃⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣻⡿⣿⣿⣿⣿⣿⣿⣿⡇⡄⠾⣲⣾⣿⣌⢿⣿⣿⣿⣿⣿⣿⣿⣧⡙⠇⠀⢻⡄⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠚⠛⠛⠛⠚⠛⠛⠛⠛⠛⠚⠛⠛⠓⠚⠒⠛⠒⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠚⠛⠛⠛⠛⠓⠛⠛⠛⠓⠛⠛⠛⠛⠓⠚⠛⠛⠓⠚⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠚⠛⠛⠛⠛⠛⠛⠓⠛⠚⠛⠛⠛⠛⠓⠛⠛⠛⠛⠛⠛⠛⠛⠛⠒⠚⠚⠁⠀⠀⠀⠀⠀⠀⠀\n']={'en':':a1:',_F:2,'E':.6,_D:[':a1:']} + +emoji.EMOJI_DATA[''] = { + "en": ":snek1:", + "status": 2, + "E": 0.6, + "alias": [":snek1:"], +} + +emoji.EMOJI_DATA[ + """⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣠⣤⡴⠶⢶⣞⣛⣛⡳⣳⠶⣶⡶⢶⢶⣦⣤⣄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⠶⠚⣫⣴⣬⠐⣶⣿⣿⣏⣽⣿⣿⣇⢿⣯⣿⣿⣻⣿⣿⣾⣮⣹⣿⢶⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⢾⣻⣽⣾⡇⢡⣿⣿⣇⡟⣿⣿⣿⣼⣿⣿⣿⣿⢸⣿⣟⣿⣷⢻⣿⣿⣿⣷⡽⣧⣹⡻⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠟⣫⣶⣿⣟⣾⡿⠀⣾⣿⣿⢹⢿⣿⣿⣏⣿⣿⣿⣿⣿⣺⣿⡏⣿⣿⣏⣿⣿⣿⣿⣿⣞⣿⣿⣯⡻⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⣠⡴⣛⣵⣻⣟⣿⢯⣿⢟⡆⣀⣿⣿⣿⣿⣾⣿⣿⢹⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣸⣿⣿⣿⣿⣿⡾⣿⣻⣿⣮⠻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⡀⠀⠀⠀⢀⣠⡾⣻⣾⡿⣳⡟⣾⢿⣿⢯⣿⡇⢸⣿⣿⣿⣇⣿⣿⡿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⢻⣏⣿⣿⡵⣝⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣟⢧⣤⣶⢾⣻⣿⣾⣿⣿⢳⣿⢻⣏⣿⢿⣿⡿⠀⣾⣿⣿⣿⢹⣿⣿⣗⣿⣿⣿⣿⣿⢏⣿⣿⠋⣿⣿⡟⡈⣿⣿⣿⠸⣿⣿⣹⣿⢸⣿⣧⢻⣮⢿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠿⠿⠿⢿⡟⣿⣿⣿⢧⣿⣟⡟⣾⣯⣿⡿⣽⡇⣿⣿⣿⣿⣸⣿⣿⠹⣿⡿⢻⡿⢣⣿⡿⣳⣖⣿⣿⢱⣿⢹⣿⣿⠀⣿⣿⠘⣿⡞⣿⣿⡸⣿⣯⢿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣼⣿⣿⡿⣼⣿⣿⢧⣿⣼⣿⣻⣿⡃⣿⣿⣿⣿⣿⣿⣿⣀⣿⢧⠘⣣⢸⡿⣻⣿⣿⡘⠟⣿⣿⡘⠿⠟⣟⣻⡿⣀⢿⡇⢻⣿⡇⣿⣿⢸⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣏⣿⣿⣿⢳⣿⣿⣿⣸⢷⣿⢷⣿⢟⢨⣿⣿⣿⣿⣿⣿⣯⣿⣦⠖⠘⡛⣡⠬⠭⠭⠛⠂⠰⣿⣿⡇⠀⣰⡏⢛⣴⣿⡏⢸⣼⡿⠇⣿⣿⣧⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣹⣿⣿⡏⣿⣿⣿⡇⣿⣾⣯⣿⣯⡿⢸⣿⣿⣿⣿⣿⢿⡃⣽⣶⣿⣿⣿⣿⣿⣿⣿⣷⣤⣗⠊⣻⣿⣶⣿⣶⣿⣿⣿⡇⣾⠏⡁⠀⣿⣿⣧⣹⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣧⣿⣿⣿⣹⣿⣿⣿⡇⣿⡿⣾⠳⠋⣄⣼⣿⣿⣿⣻⣾⣿⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣫⣤⣄⣀⣿⠇⣿⣿⣿⣻⡌⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣸⣿⣿⣏⣿⣿⣿⣿⣿⣿⣷⣇⢀⣾⡋⣽⣿⣿⣿⣿⣿⣿⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣱⣿⣿⣿⣿⣿⡇⣬⣻⣿⣽⡇⠸⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣇⣿⣿⣿⣹⣿⣿⢏⣅⢀⠲⣤⣙⠼⢿⡇⣿⣿⣿⣿⣿⡿⠿⠻⠟⠻⠿⠿⣷⣽⡻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣵⣿⣿⣿⣿⣿⣿⡇⣿⣧⣿⢾⡇⠀⢿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⢸⣿⣿⡿⣾⣿⣏⣾⠇⠈⠀⠈⢻⣿⣷⡁⣿⣿⣿⣿⣿⣧⣤⣴⣶⠀⠀⠀⠀⣊⡙⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⣻⣿⡇⣿⣿⣿⣻⡇⠀⢸⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡏⣿⣿⣿⡗⣿⣿⢸⡟⣼⣀⠏⠁⡀⢿⣿⡇⣹⣿⣿⣿⣿⣿⡇⢿⣯⠀⡄⠀⠁⠉⠙⣿⣮⡝⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠁⠀⣀⣀⡀⠁⢿⣻⣿⣿⡇⠀⠈⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢣⣿⣿⣿⢸⣿⣏⢾⡧⡿⠋⠉⢁⣡⣿⣿⣧⣿⣿⣿⣿⣿⣿⣃⣷⣝⣀⣽⣤⣧⣤⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⣀⠀⠀⠉⢩⣿⡇⣶⣳⣿⣽⡇⠀⢠⡿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣸⣿⣿⡿⣼⣿⣷⠸⠃⢻⡜⢿⣿⢟⣿⣿⣿⣿⣿⣿⣿⣿⣿⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣿⣷⣥⣤⣥⣤⣿⣿⡅⣿⣿⣿⣿⡇⠀⣼⣿⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⣿⣿⣿⣿⣿⣿⣿⣄⢷⣄⣉⠮⠉⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⢼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⢹⣿⣿⣿⣿⣿⣿⣿⡇⣿⣿⣿⣿⡇⢼⣿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢧⣿⣿⣿⣿⣿⣿⣿⣿⣮⢿⣿⣿⣷⣦⡈⢿⣿⣿⣿⣿⣿⣿⣿⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢇⣿⣿⣿⣿⣿⣿⣿⣇⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣵⡝⢿⣿⣿⣿⣶⣍⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢉⣾⣿⣿⣿⣿⣿⣿⣿⢹⣿⣿⣿⢿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⡟⣾⣿⣿⣿⣿⣿⣿⣿⣿⡟⣿⢻⢳⢸⢩⣟⣛⠓⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣿⣿⣿⣿⣿⣿⣿⡿⢸⣿⣿⣿⣼⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⣿⣿⣿⣿⣿⠇⣿⣿⣿⠇⣿⢸⢸⣸⡇⢸⣿⠇⣿⣿⣿⣿⣿⣿⢹⣿⣯⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⢿⢿⠛⢻⣿⣿⣿⣿⣿⣿⣿⠃⢸⣿⣿⣿⢾⡃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣷⣿⣿⣿⣿⣿⠀⣿⣻⣿⢹⣿⣸⠘⡿⡇⢸⣿⡄⢿⣿⣿⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢛⣵⣶⣶⣬⠀⢠⣿⣿⣿⣿⣿⣿⣿⠏⢄⣸⣿⣿⣿⣿⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⢸⣿⣿⣿⣿⡟⢸⡿⣿⣿⢸⡇⡖⡄⠇⣇⢸⣿⡓⢻⣿⣿⣿⣿⣿⢺⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿⣿⣿⣿⣿⢧⣿⣿⣿⣿⣿⣿⣿⠋⢨⠀⣿⣿⣿⣏⣿⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡏⣾⣿⣿⣿⣿⠃⣼⣷⣿⡏⣿⣿⠇⢹⢸⣿⢺⣿⣿⢸⣿⣿⣿⣿⣿⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⣙⡛⣛⢡⣾⣿⠻⢿⣿⣿⣟⣱⡆⠨⠀⣿⣿⢿⡗⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣹⣿⣿⣿⣿⣿⠀⣼⣹⣿⢣⣿⣿⢠⣸⣼⣿⡞⣿⣿⣾⣿⣿⣿⣿⣿⢸⣦⣟⢿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣷⣾⢿⣿⣿⠈⢷⡝⢻⡊⠁⣧⠀⠃⣿⣿⡆⢋⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢯⣿⣿⣿⣿⣿⣿⡄⣿⣿⣿⣸⣿⣿⣸⢻⣿⣿⡗⣿⣿⡏⣿⣿⣿⣿⣿⠈⣿⣿⣬⣙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⡆⢨⣁⣘⣷⣄⣿⠀⢠⣿⣿⣣⣈⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣟⣿⣿⣿⣿⣿⣿⣿⢀⠯⣿⡏⣿⣿⡇⢻⣽⣿⣿⣟⣻⣿⡇⣿⣿⡏⣿⣿⣰⡼⢺⡾⣏⣿⣨⡟⠿⣿⣿⣿⣿⣿⣿⣿⢟⡉⡆⡎⠿⣿⣿⣮⡻⡇⢸⣿⣏⢿⣿⣞⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣟⣾⣿⣿⢇⣾⣿⣿⢋⣠⢰⣿⢿⣿⣿⢰⢸⢹⣿⣿⣿⣸⣿⣧⣿⣿⡇⣿⣿⢼⣿⣦⡹⢹⡜⣇⢿⣹⣶⣍⡛⠻⠿⣫⡖⡟⣠⣱⣧⣿⣞⣻⣿⣿⣦⡸⣿⣿⡜⣿⣿⣯⢿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢟⣾⣿⣿⢏⣾⣿⣿⡏⣾⡏⢸⣿⣾⣿⡇⠆⣿⢸⣿⣿⣿⡏⣿⣷⢹⣿⣷⣿⣿⢸⣿⣿⣿⣦⡻⡸⡞⣧⢿⠧⣇⠙⠿⣿⣼⢣⠃⢋⠃⣿⠈⠳⣝⢿⣿⣿⣮⣝⢿⡼⣿⣿⣷⡩⣽⣻⢶⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⢯⣿⣿⡿⣳⣿⣿⣿⡿⢸⣿⠇⣺⣿⢯⣿⡝⣸⡿⡘⣽⣿⣿⣗⢿⣿⢸⣿⣿⣿⣿⠦⣿⣿⣿⣿⣿⣶⣭⣘⠎⠘⢣⣷⡆⠂⠥⠁⣂⡟⠟⢿⣀⠀⠉⠳⣝⢿⣿⣿⣿⣦⣙⣿⣿⣿⣎⣿⣷⣝⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡾⣵⣿⣿⣏⢼⣿⣽⣿⡿⣁⣿⡿⠸⠹⢫⣿⣟⣼⣿⠃⣵⣿⡿⢸⣿⠘⣿⠘⣿⡏⣿⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⢲⣸⡿⣳⣮⣝⡿⣿⣿⣿⣾⣿⣿⣿⣶⣿⣷⡎⠻⣿⣿⣿⣿⣷⣿⡻⢸⡿⣿⣿⣽⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢟⣼⡿⣯⡿⣯⣿⢳⣿⣿⠗⡋⠁⠀⠀⠈⠒⢦⣝⠻⣷⣿⡿⢟⡥⣼⣿⠀⠹⢀⣿⡿⣿⣿⡏⢹⣿⣿⣿⣿⣿⣿⣿⣿⠸⢺⡇⡇⣿⢸⣿⡆⣭⣛⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⡼⡇⣿⣿⣿⣯⢷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡾⣣⡿⢋⡾⠋⣼⢳⣿⢿⠟⠡⢈⣥⠶⠟⠛⠿⠷⣦⣽⣷⣤⡐⢾⣿⣷⣿⢏⣀⡀⠀⣿⣷⣿⣿⣿⢨⢿⣿⣿⣿⣿⣿⣿⣿⢸⠾⡧⣿⢿⣸⣿⣶⢸⣿⣿⡗⠶⢯⣭⣛⡻⡿⠿⢿⣿⣿⣿⣭⣝⣻⡇⣻⣿⣻⢿⣿⣧⠙⢷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣟⣽⢟⣵⠟⠀⢰⡿⣾⡿⢉⣶⡾⠋⠁⠀⠀⠀⠀⠀⠀⠉⠙⠿⣿⣦⠈⠉⠠⠀⠻⣿⠀⣿⣿⣿⣿⣿⣺⣟⣿⣿⣿⣿⣿⣿⡇⢸⣶⡟⢿⡼⠇⠿⣱⠘⣿⣧⣷⠀⠀⢀⣩⡇⠿⣟⣃⣬⣿⣿⣿⣿⣿⡇⣿⢸⣿⣷⣽⣿⣯⡨⠻⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⢿⣾⣷⠟⠁⠀⠀⣾⢿⣿⠃⡞⣩⣶⣿⣿⣿⣿⣷⣦⡄⠀⠀⠀⠀⣠⣼⣷⡀⠀⢣⠀⠘⠃⣿⣿⣿⣿⣿⣏⣿⣞⣿⣿⣿⣿⣿⡇⠀⠈⠑⠾⢧⣾⡞⣵⣷⢻⣿⠻⣶⣟⣿⣽⣶⣿⣿⣿⣿⣿⣿⣿⣽⣿⡇⣿⠈⣿⣿⣿⣿⣻⣧⢱⣜⢿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⡿⠁⠀⠀⠀⢸⡟⣿⠇⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⣴⣿⣿⣿⣿⣄⠀⣃⠀⠀⣿⣿⢹⣿⣿⣇⢿⣿⣞⢿⣿⣿⣿⡇⢠⣤⣤⣄⡀⢤⣬⠁⠈⠁⣿⣾⣿⣿⡿⢿⣻⢛⣭⣿⢏⣼⣿⣿⣿⣿⠇⣮⣤⣿⣿⣿⣿⣿⣿⣇⢿⣎⢿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠋⠀⠀⠀⠀⢀⣿⢻⡟⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀⠀⣼⣿⣿⣿⣿⣿⣿⣦⠘⡇⡀⣿⣿⢸⣿⣿⣿⠸⣿⣿⣾⣿⣿⣿⡇⢰⣬⣝⡻⢿⣇⣂⣬⣤⣙⡋⠭⢥⣀⣀⠈⠙⠃⣫⣷⣿⣿⣿⣿⣭⣿⢢⣿⣿⣿⣿⣿⣿⣿⣿⣿⠈⣿⣎⢿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⡟⣾⢁⣾⣿⠉⠙⣿⣿⣿⣿⣿⣿⡿⠁⠀⣼⣿⣿⣿⣿⡿⠿⣿⣿⡇⢁⣿⢹⣿⣾⣿⣿⣿⡄⣜⢿⣿⣿⣿⣿⣿⠀⠻⣿⣿⣶⣍⠻⣿⣿⣿⣿⡿⢃⣾⠏⣠⣴⣿⣿⡿⠿⣛⢩⡜⢿⢏⢼⣿⣿⣿⣿⣿⣾⢿⣿⣿⠀⣿⢻⣎⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⢹⡟⣸⣿⡇⠀⠀⠈⢿⣿⣿⡿⠋⠀⠀⢠⣿⡿⠟⠉⠁⣠⡾⢋⣿⣧⠘⠻⢸⣿⣏⣿⣿⣿⣧⢉⣓⣈⢛⡿⠿⣿⣦⣶⣿⣿⣿⠿⣂⣤⡽⣿⠏⣴⡿⢋⣾⡿⠟⠋⠐⣾⣿⣿⡄⠻⣶⢿⣣⣮⣝⢿⣿⣿⣿⣷⡻⣿⠀⡟⠈⣿⡜⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⡏⣿⡇⣿⣿⠃⠀⠀⠀⠀⠙⠉⠀⠀⠀⠀⠘⠉⠀⣀⣴⡾⢋⣵⣿⣿⣧⡄⠀⢸⣿⣿⣿⣿⣿⣿⡄⠙⠛⢓⣙⢿⣷⣾⣿⣿⣿⣵⣾⣿⣿⣧⣄⣸⠋⢤⡬⠥⠀⠀⢶⡄⠈⠹⣿⣇⢀⠈⢝⣿⣿⣿⣿⡽⣿⣿⣿⡿⠁⢠⡇⠀⢻⣿⣹⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⡿⣸⣿⣷⠸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⣾⠟⣩⣶⣿⣿⣿⣿⣿⣿⠀⢸⣿⣿⣿⣿⣿⣿⣧⠸⠛⣛⣯⣤⣿⡻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣔⢶⣶⣷⡀⢻⣄⠀⠙⠿⣎⠀⠈⠻⣿⣿⣿⣿⣟⣿⢿⠁⠀⣼⠀⠀⠘⣿⣧⢻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⢣⣿⣿⣿⡇⠃⠀⠀⠀⠀⠀⠀⠀⣀⣤⣾⣿⡿⢛⣥⣾⣿⣿⣿⣿⣿⣿⣿⡟⠀⢸⣿⣿⣿⣿⣿⣿⣿⡄⢶⣭⣙⡛⠿⢿⣾⣝⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⡻⢿⣀⠙⠂⠀⣀⠈⠑⠀⠀⠀⢠⣮⠍⡉⢠⡌⠁⠀⠇⢀⣤⡀⢻⡟⢏⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣏⣿⣿⣿⣿⣠⠈⠀⠀⢀⣠⣴⣾⣿⣿⣿⠟⣋⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠁⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⡀⠉⡛⢿⣷⣦⣬⣉⠓⢮⡻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣝⠦⠀⠂⠙⣷⣄⡀⠀⠀⠀⠀⠀⠐⠀⠀⠀⣶⠀⣸⣿⡇⠸⣇⠘⡜⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡟⣼⣿⣿⣿⡏⣿⢰⣶⣿⠛⠛⠛⠉⠉⠁⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⢻⣿⣧⠀⢹⣷⣮⠛⣿⣿⣿⣦⡌⠐⡝⢻⣿⣿⣿⣶⣽⣿⣿⣿⣿⣿⣷⡄⠀⢠⢹⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⣼⠃⠀⠘⣿⠃⠀⣭⠀⢻⠙⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⢳⣿⣿⣿⣿⣷⣿⡆⣿⠃⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠀⢠⡇⢰⠂⣿⣿⣿⣿⣿⣿⡈⢿⣿⡄⠈⣿⣿⣷⣤⡉⠛⠿⣿⣷⣦⣔⡙⠻⣿⣿⣿⣿⣝⢿⣿⣿⣿⣿⣦⡀⢈⢻⣿⣿⣿⣄⢤⣀⠀⢀⣴⠏⠀⠀⠀⠀⠀⣾⣿⡇⢸⣧⢻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⢯⣿⣿⣿⣿⣿⣿⣿⡇⣇⢀⣀⣤⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⣡⠄⠀⣼⡇⢺⠁⣿⣿⣧⣿⣿⣿⡇⠈⢿⣿⡀⢹⣿⣿⣿⣿⣯⣻⣶⣯⣽⣿⠿⣿⣾⣽⣻⢿⣿⣿⣽⣿⢿⣿⣿⣿⣞⣇⢙⢿⣿⣿⣯⣻⣿⣶⣤⣀⠀⠀⠀⠀⠀⢻⡿⠁⠈⣿⣧⢻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣏⣿⣿⣿⣿⢿⣿⣿⡿⢃⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠁⢹⣿⣿⣿⣿⡟⠀⣸⣿⡇⠈⠀⣿⣿⣿⣿⡏⣿⣧⠀⠀⠻⣷⡜⣿⣿⣿⣿⣿⣿⣷⣽⡻⣿⣿⣷⣿⣽⣛⡿⢾⣽⣛⢿⣿⣾⣝⡿⣿⣯⡃⢣⡻⣿⣿⣷⡽⣿⣿⣿⣷⣦⣀⠀⠀⠀⠀⠀⠀⢿⣿⣧⢻⡄⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣟⣿⣿⢯⣿⢏⣾⣿⣿⢃⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠀⣠⣿⣿⣿⣿⣿⠀⣸⣿⣿⡇⠀⠀⣿⣿⢹⣿⡇⢻⣿⠀⠀⢠⡌⠿⣜⢿⣿⣿⣿⣿⣿⣿⣿⣾⣝⡿⣿⣿⣿⣿⣿⣶⣯⣝⣊⠽⢛⠿⣷⣝⢷⣤⠳⡜⢿⣿⣿⣝⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⢸⣿⣿⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⡟⣾⣿⣯⣿⢣⣾⣿⣿⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⣠⣼⣿⣿⣿⣿⣿⠃⣰⣿⣿⣿⡇⡄⠀⣿⣿⢸⣿⡇⠈⢿⡇⠀⣿⠇⣷⣯⣃⣙⣛⣛⣵⣿⣿⣿⣿⣿⣿⣾⣝⡻⣿⣿⣿⣿⣿⣿⣿⣷⣮⣄⡈⠳⢜⠃⠘⢎⢿⣿⣿⣞⢿⣿⣿⣿⣿⣿⣷⣆⡀⠀⠘⣿⡟⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣟⣽⣿⣯⣿⣣⣿⣿⣿⣿⣿⡄⢿⣿⣿⣿⣿⣿⣿⡿⠟⣡⣾⣿⣿⣿⣿⣿⣿⡏⣰⣿⣿⡿⢹⡇⣾⠀⣿⣿⣾⣿⡇⠀⠈⢷⢰⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣽⣻⢿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣦⣀⡀⠈⠛⠻⢿⣎⢿⣿⣿⣿⣿⣿⣿⣷⣄⠀⣿⡇⠘⣧⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⢟⣾⣿⣻⡿⣵⣿⣿⣿⣿⣟⣾⣯⠌⠛⠛⠛⠛⠉⠁⢠⣾⣿⣿⣿⣿⣿⣿⡿⠏⣰⣿⣿⡿⠁⢸⡇⢿⠀⣿⣿⣿⣿⡇⠀⠠⠈⠂⢃⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣻⡿⣿⣿⣿⣿⣿⣿⣿⡇⡄⠾⣲⣾⣿⣌⢿⣿⣿⣿⣿⣿⣿⣿⣧⡙⠇⠀⢻⡄⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠚⠛⠛⠛⠚⠛⠛⠛⠛⠛⠚⠛⠛⠓⠚⠒⠛⠒⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠚⠛⠛⠛⠛⠓⠛⠛⠛⠓⠛⠛⠛⠛⠓⠚⠛⠛⠓⠚⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠚⠛⠛⠛⠛⠛⠛⠓⠛⠚⠛⠛⠛⠛⠓⠛⠛⠛⠛⠛⠛⠛⠛⠛⠒⠚⠚⠁⠀⠀⠀⠀⠀⠀⠀ +""" +] = {"en": ":a1:", "status": 2, "E": 0.6, "alias": [":a1:"]} + + def set_link_target_blank(text): - soup=BeautifulSoup(text,_B) - for element in soup.find_all('a'):element.attrs['target']='_blank';element.attrs['rel']='noopener noreferrer';element.attrs['referrerpolicy']='no-referrer';element.attrs[_A]=element.attrs[_A].strip('.').strip(',') - return str(soup) + soup = BeautifulSoup(text, "html.parser") + + for element in soup.find_all("a"): + element.attrs["target"] = "_blank" + element.attrs["rel"] = "noopener noreferrer" + element.attrs["referrerpolicy"] = "no-referrer" + element.attrs["href"] = element.attrs["href"].strip(".").strip(",") + + return str(soup) + + def embed_youtube(text): - soup=BeautifulSoup(text,_B) - for element in soup.find_all('a'): - if element.attrs[_A].startswith('https://www.you'): - video_name=element.attrs[_A].split('/')[-1] - if'v='in element.attrs[_A]:video_name=element.attrs[_A].split('?v=')[1].split('&')[0] - embed_template=f'';element.replace_with(BeautifulSoup(embed_template,_B)) - return str(soup) + soup = BeautifulSoup(text, "html.parser") + for element in soup.find_all("a"): + if element.attrs["href"].startswith("https://www.you"): + video_name = element.attrs["href"].split("/")[-1] + if "v=" in element.attrs["href"]: + video_name = element.attrs["href"].split("?v=")[1].split("&")[0] + # if "si=" in element.attrs["href"]: + # video_name = "?v=" + element.attrs["href"].split("/")[-1] + # if "t=" in element.attrs["href"]: + # video_name += "&t=" + element.attrs["href"].split("&t=")[1].split("&")[0] + embed_template = f'' + element.replace_with(BeautifulSoup(embed_template, "html.parser")) + return str(soup) + + def embed_image(text): - soup=BeautifulSoup(text,_B) - for element in soup.find_all('a'): - for extension in['.png','.jpg','.jpeg','.gif','.webp','.svg','.bmp','.tiff','.ico','.heif']: - if extension in element.attrs[_A].lower():embed_template=f'{element.attrs[_A]}';element.replace_with(BeautifulSoup(embed_template,_B)) - return str(soup) + soup = BeautifulSoup(text, "html.parser") + for element in soup.find_all("a"): + for extension in [ + ".png", + ".jpg", + ".jpeg", + ".gif", + ".webp", + ".svg", + ".bmp", + ".tiff", + ".ico", + ".heif", + ]: + if extension in element.attrs["href"].lower(): + embed_template = f'{element.attrs[' + element.replace_with(BeautifulSoup(embed_template, "html.parser")) + return str(soup) + + def embed_media(text): - soup=BeautifulSoup(text,_B) - for element in soup.find_all('a'): - for extension in['.mp4','.mp3','.wav','.ogg','.webm','.flac','.aac','.mpg','.avi','.wmv']: - if extension in element.attrs[_A].lower():embed_template=f'';element.replace_with(BeautifulSoup(embed_template,_B)) - return str(soup) + soup = BeautifulSoup(text, "html.parser") + for element in soup.find_all("a"): + for extension in [ + ".mp4", + ".mp3", + ".wav", + ".ogg", + ".webm", + ".flac", + ".aac", + ".mpg", + ".avi", + ".wmv", + ]: + if extension in element.attrs["href"].lower(): + embed_template = f'' + element.replace_with(BeautifulSoup(embed_template, "html.parser")) + return str(soup) + + def linkify_https(text): - if'https://'not in text:return text - url_pattern='(?()]+(?">\\g<0>',element);element.replace_with(BeautifulSoup(new_text,_B)) - return set_link_target_blank(str(soup)) + if "https://" not in text: + return text + url_pattern = r'(?()]+(?">\g<0>', element) + element.replace_with(BeautifulSoup(new_text, "html.parser")) + + return set_link_target_blank(str(soup)) + + class EmojiExtension(Extension): - tags={'emoji'} - def parse(self,parser): - line_number=next(parser.stream).lineno;md_file=[Const('')];body='' - try:md_file=[parser.parse_expression()] - except TemplateSyntaxError:body=parser.parse_statements(['name:endemoji'],drop_needle=_C) - return nodes.CallBlock(self.call_method(_E,md_file),[],[],body).set_lineno(line_number) - def _to_html(self,md_file,caller):return emoji.emojize(caller(),language=_D) + tags = {"emoji"} + + def parse(self, parser): + line_number = next(parser.stream).lineno + md_file = [Const("")] + body = "" + try: + md_file = [parser.parse_expression()] + except TemplateSyntaxError: + body = parser.parse_statements(["name:endemoji"], 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 emoji.emojize(caller(), language="alias") + + class LinkifyExtension(Extension): - tags={'linkify'} - def __init__(self,environment):self.app=SimpleNamespace(jinja2_env=environment);super(LinkifyExtension,self).__init__(environment) - def parse(self,parser): - line_number=next(parser.stream).lineno;md_file=[Const('')];body='' - try:md_file=[parser.parse_expression()] - except TemplateSyntaxError:body=parser.parse_statements(['name:endlinkify'],drop_needle=_C) - return nodes.CallBlock(self.call_method(_E,md_file),[],[],body).set_lineno(line_number) - def _to_html(self,md_file,caller):result=linkify_https(caller());result=embed_media(result);result=embed_image(result);result=embed_youtube(result);return result + tags = {"linkify"} + + def __init__(self, environment): + self.app = SimpleNamespace(jinja2_env=environment) + super(LinkifyExtension, self).__init__(environment) + + def parse(self, parser): + line_number = next(parser.stream).lineno + md_file = [Const("")] + body = "" + try: + md_file = [parser.parse_expression()] + except TemplateSyntaxError: + body = parser.parse_statements(["name:endlinkify"], 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): + result = linkify_https(caller()) + result = embed_media(result) + result = embed_image(result) + result = embed_youtube(result) + return result + + class PythonExtension(Extension): - tags={'py3'} - def parse(self,parser): - line_number=next(parser.stream).lineno;md_file=[Const('')];body='' - try:md_file=[parser.parse_expression()] - except TemplateSyntaxError:body=parser.parse_statements(['name:endpy3'],drop_needle=_C) - return nodes.CallBlock(self.call_method(_E,md_file),[],[],body).set_lineno(line_number) - def _to_html(self,md_file,caller): - def fn(source): - import subprocess - def system(command): - if isinstance(command):command=command.split(' ') - from io import StringIO;stdout=StringIO();subprocess.run(command,stderr=stdout,stdout=stdout,text=_C);return stdout.getvalue() - to_write=[] - def render(text):global to_write;to_write.append(text) - exec(source);return''.join(to_write) - return str(fn(caller())) \ No newline at end of file + tags = {"py3"} + + def parse(self, parser): + line_number = next(parser.stream).lineno + md_file = [Const("")] + body = "" + try: + md_file = [parser.parse_expression()] + except TemplateSyntaxError: + body = parser.parse_statements(["name:endpy3"], 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): + + def fn(source): + import subprocess + + def system(command): + if isinstance(command): + command = command.split(" ") + from io import StringIO + + stdout = StringIO() + subprocess.run(command, stderr=stdout, stdout=stdout, text=True) + return stdout.getvalue() + + to_write = [] + + def render(text): + global to_write + to_write.append(text) + + exec(source) + return "".join(to_write) + + return str(fn(caller())) diff --git a/src/snek/system/terminal.py b/src/snek/system/terminal.py index 82207c7..c5410b6 100644 --- a/src/snek/system/terminal.py +++ b/src/snek/system/terminal.py @@ -1,49 +1,113 @@ -_A=None -import asyncio,os -try:import pty -except Exception as ex:print('You are not able to run a terminal. See error:');print(ex) +import asyncio +import os + +try: + import pty +except Exception as ex: + print("You are not able to run a terminal. See error:") + print(ex) 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: - 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 start_process(A,command): - if not A.is_running(): - if A.master:os.close(A.master);os.close(A.slave);A.master=_A;A.slave=_A - 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) - def is_running(A): - if not A.process:return False - asyncio.get_event_loop();return A.process.poll()is _A - async def add_websocket(A,ws):A.start_process(A.command);asyncio.create_task(A.read_output(ws)) - async def read_output(A,ws): - B=ws;A.sockets.append(B) - if len(A.sockets)>1 and A.history: - D=0 - try:D=A.history.index(b'\n') - except ValueError:pass - await B.send_bytes(A.history[D:]);return - E=asyncio.get_event_loop() - while True: - try: - C=await E.run_in_executor(_A,os.read,A.master,1024) - if not C:break - A.history+=C - if len(A.history)>A.history_size:A.history=A.history[:0-A.history_size] - try: - for B in A.sockets:await B.send_bytes(C) - except:A.sockets.remove(B) - except Exception:await A.close();break - async def close(A): - print('Terminating process') - if A.process:A.process.terminate();A.process=_A - if A.master:os.close(A.master);os.close(A.slave);A.master=_A;A.slave=_A - print('Terminated process') - for B in A.sockets: - try:await B.close() - except Exception:pass - A.sockets=[] - async def write_input(B,data): - A=data - try:A=A.encode() - except AttributeError:pass - try:await asyncio.get_event_loop().run_in_executor(_A,os.write,B.master,A) - except Exception as C:print(C);await B.close() \ No newline at end of file + def __init__(self, command): + self.master, self.slave = None, None + self.process = None + self.sockets = [] + self.history = b"" + self.history_size = 1024 * 20 + self.command = command + self.start_process(self.command) + + def start_process(self, command): + if not self.is_running(): + if self.master: + os.close(self.master) + os.close(self.slave) + self.master = None + self.slave = None + + self.master, self.slave = pty.openpty() + self.process = subprocess.Popen( + command.split(" "), + stdin=self.slave, + stdout=self.slave, + stderr=self.slave, + bufsize=0, + universal_newlines=True, + ) + + def is_running(self): + if not self.process: + return False + asyncio.get_event_loop() + return self.process.poll() is None + + async def add_websocket(self, ws): + self.start_process(self.command) + asyncio.create_task(self.read_output(ws)) + + async def read_output(self, ws): + self.sockets.append(ws) + if len(self.sockets) > 1 and self.history: + start = 0 + 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() diff --git a/src/snek/system/view.py b/src/snek/system/view.py index be19178..70379ef 100644 --- a/src/snek/system/view.py +++ b/src/snek/system/view.py @@ -1,31 +1,75 @@ from aiohttp import web + from snek.system.markdown import render_markdown + + class BaseView(web.View): - login_required=False - async def _iter(A): - if A.login_required and(not A.session.get('logged_in')or not A.session.get('uid')):return web.HTTPFound('/') - return await super()._iter() - @property - def base_url(self):return str(self.request.url.with_path('').with_query('')) - @property - def app(self):return self.request.app - @property - def db(self):return self.app.db - @property - def services(self):return self.app.services - async def json_response(B,data,**A):return web.json_response(data,**A) - @property - def session(self):return self.request.session - async def render_template(A,template_name,context=None): - C=context;B=template_name - 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) + + login_required = False + + async def _iter(self): + if self.login_required and ( + not self.session.get("logged_in") or not self.session.get("uid") + ): + return web.HTTPFound("/") + return await super()._iter() + + @property + def base_url(self): + return str(self.request.url.with_path("").with_query("")) + + @property + def app(self): + return self.request.app + + @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): - form=None - async def get(A):B=A.form(app=A.app);return await A.json_response(await B.to_json()) - 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() - if D.get(E)=='validate':0 - 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) - async def submit(A,model=None):0 \ No newline at end of file + + form = None + + async def get(self): + form = self.form(app=self.app) + + return await self.json_response(await form.to_json()) + + 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 diff --git a/src/snek/view/about.py b/src/snek/view/about.py index 740ec7a..aba57ae 100644 --- a/src/snek/view/about.py +++ b/src/snek/view/about.py @@ -1,5 +1,39 @@ +# Written by retoor@molodetz.nl + +# This source code defines two classes, `AboutHTMLView` and `AboutMDView`, both inheriting from `BaseView`. They asynchronously return rendered templates for HTML and Markdown respectively. + +# External Import: `BaseView` from `snek.system.view` + +# 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.view import BaseView + + class AboutHTMLView(BaseView): - async def get(A):return await A.render_template('about.html') + + async def get(self): + return await self.render_template("about.html") + + class AboutMDView(BaseView): - async def get(A):return await A.render_template('about.md') \ No newline at end of file + + async def get(self): + return await self.render_template("about.md") diff --git a/src/snek/view/avatar.py b/src/snek/view/avatar.py index c95384a..a85b876 100644 --- a/src/snek/view/avatar.py +++ b/src/snek/view/avatar.py @@ -1,10 +1,44 @@ +# Written by retoor@molodetz.nl + +# This code defines a WebView class that inherits from BaseView and includes a method for rendering a web template, requiring login access for its usage. + +# The code imports the BaseView class from the `snek.system.view` module. + +# 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. import uuid + from aiohttp import web from multiavatar import multiavatar + from snek.system.view import BaseView + + class AvatarView(BaseView): - login_required=False - async def get(C): - A=C.request.match_info.get('uid') - if A=='unique':A=str(uuid.uuid4()) - D=multiavatar.multiavatar(A,True,None);B=web.Response(text=D,content_type='image/svg+xml');B.headers['Cache-Control']=f"public, max-age={56154}";return B \ No newline at end of file + login_required = False + + async def get(self): + uid = self.request.match_info.get("uid") + if uid == "unique": + uid = str(uuid.uuid4()) + avatar = multiavatar.multiavatar(uid, True, None) + response = web.Response(text=avatar, content_type="image/svg+xml") + response.headers["Cache-Control"] = f"public, max-age={1337*42}" + return response diff --git a/src/snek/view/docs.py b/src/snek/view/docs.py index ce1e31c..bb63413 100644 --- a/src/snek/view/docs.py +++ b/src/snek/view/docs.py @@ -1,5 +1,37 @@ +# Written by retoor@molodetz.nl + +# This code defines two classes, DocsHTMLView and DocsMDView, which are intended to asynchronously render HTML and Markdown templates respectively. Both classes inherit from the BaseView class. + +# Dependencies: BaseView is imported from the "snek.system.view" package. + +# 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.view import BaseView + + class DocsHTMLView(BaseView): - async def get(A):return await A.render_template('docs.html') + + async def get(self): + return await self.render_template("docs.html") + + class DocsMDView(BaseView): - async def get(A):return await A.render_template('docs.md') \ No newline at end of file + + async def get(self): + return await self.render_template("docs.md") diff --git a/src/snek/view/drive.py b/src/snek/view/drive.py index 630cc3a..e3c3343 100644 --- a/src/snek/view/drive.py +++ b/src/snek/view/drive.py @@ -1,99 +1,269 @@ -_P='Path not found' -_O='application/octet-stream' -_N='items' -_M='size' -_L='mimetype' -_K='name' -_J='rel_path' -_I='dir' -_H='url' -_G=None -_F='path' -_E='status' -_D='file' -_C='uid' -_B='absolute_url' -_A='type' from aiohttp import web + from snek.system.view import BaseView -import os,mimetypes + + +import os +import mimetypes from aiohttp import web -from urllib.parse import unquote,quote +from urllib.parse import unquote, quote from datetime import datetime -'Run with: python server.py (Python\xa0≥\xa03.9)\nVisit http://localhost:8080 to try the demo.\n' + + + +"""Run with: python server.py (Python ≥ 3.9) +Visit http://localhost:8080 to try the demo. +""" from aiohttp import web from pathlib import Path -import mimetypes,urllib.parse -BASE_DIR=Path(__file__).parent.resolve() -ROOT_DIR=(BASE_DIR/'storage').resolve() -ASSETS_DIR=(BASE_DIR/'assets').resolve() +import mimetypes, urllib.parse + +# ---------- Configuration -------------------------------------------------- +BASE_DIR = Path(__file__).parent.resolve() +ROOT_DIR = (BASE_DIR / "storage").resolve() # files shown to the outside world +ASSETS_DIR = (BASE_DIR / "assets").resolve() # JS & demo HTML ROOT_DIR.mkdir(exist_ok=True) ASSETS_DIR.mkdir(exist_ok=True) -def safe_resolve_path(rel): - 'Return *absolute* path inside ROOT_DIR or raise FileNotFoundError.';A=(ROOT_DIR/rel.lstrip('/')).resolve() - if A==ROOT_DIR or ROOT_DIR in A.parents:return A - raise FileNotFoundError('Unsafe path') + +# ---------- Helpers -------------------------------------------------------- + +def safe_resolve_path(rel: str) -> Path: + """Return *absolute* path inside ROOT_DIR or raise FileNotFoundError.""" + target = (ROOT_DIR / rel.lstrip("/")).resolve() + if target == ROOT_DIR or ROOT_DIR in target.parents: + return target + raise FileNotFoundError("Unsafe path") + +# ---------- API view ------------------------------------------------------- + class DriveView(BaseView): - async def get(C): - H='limit';I='offset';D=C.request.query.get(_F,'');E=int(C.request.query.get(I,0));J=int(C.request.query.get(H,20));A=await C.services.user.get_home_folder(C.session.get(_C)) - if D:A.joinpath(D) - if not A.exists():return web.json_response({'error':'Not found'},status=404) - if A.is_dir(): - F=[] - for B in sorted(A.iterdir(),key=lambda p:(p.is_file(),p.name.lower())):K=(Path(D)/B.name).as_posix();M=mimetypes.guess_type(B.name)[0]if B.is_file()else'inode/directory';G=C.request.url.with_path(f"/drive/{urllib.parse.quote(K)}")if B.is_file()else _G;F.append({_K:B.name,_A:'directory'if B.is_dir()else _D,_L:M,_M:B.stat().st_size if B.is_file()else _G,_F:K,_H:G}) - import json as L;N=len(F);O=F[E:E+J];return web.json_response({_N:L.loads(L.dumps(O,default=str)),'pagination':{I:E,H:J,'total':N}}) - with open(A,'rb')as P:Q=P.read();return web.Response(body=Q,content_type=mimetypes.guess_type(A.name)[0]) - G=C.request.url.with_path(f"/drive/{urllib.parse.quote(D)}");return web.json_response({_K:A.name,_A:_D,_L:mimetypes.guess_type(A.name)[0],_M:A.stat().st_size,_F:D,_H:str(G)}) + async def get(self): + rel = self.request.query.get("path", "") + offset = int(self.request.query.get("offset", 0)) + limit = int(self.request.query.get("limit", 20)) + target = await self.services.user.get_home_folder(self.session.get("uid")) + if rel: + target.joinpath(rel) + + if not target.exists(): + return web.json_response({"error": "Not found"}, status=404) + + # ---- Directory listing ------------------------------------------- + if target.is_dir(): + entries = [] + # Directories first, then files – both alphabetical (case‑insensitive) + for p in sorted(target.iterdir(), key=lambda p: (p.is_file(), p.name.lower())): + item_path = (Path(rel) / p.name).as_posix() + mime = mimetypes.guess_type(p.name)[0] if p.is_file() else "inode/directory" + url = (self.request.url.with_path(f"/drive/{urllib.parse.quote(item_path)}") + if p.is_file() else None) + entries.append({ + "name": p.name, + "type": "directory" if p.is_dir() else "file", + "mimetype": mime, + "size": p.stat().st_size if p.is_file() else None, + "path": item_path, + "url": url, + }) + import json + total = len(entries) + items = entries[offset:offset+limit] + return web.json_response({ + "items": json.loads(json.dumps(items,default=str)), + "pagination": {"offset": offset, "limit": limit, "total": total} + }) + + with open(target, "rb") as f: + content = f.read() + return web.Response(body=content, content_type=mimetypes.guess_type(target.name)[0]) + # ---- Single file metadata ---------------------------------------- + url = self.request.url.with_path(f"/drive/{urllib.parse.quote(rel)}") + return web.json_response({ + "name": target.name, + "type": "file", + "mimetype": mimetypes.guess_type(target.name)[0], + "size": target.stat().st_size, + "path": rel, + "url": str(url), + }) + + + + + + + + + + + + + + + + + + + + + + + + class DriveView222(BaseView): - PAGE_SIZE=20 - async def base_path(A):return await A.services.user.get_home_folder(A.session.get(_C)) - async def get_full_path(C,rel_path): - A=await C.base_path();D=os.path.normpath(unquote(rel_path or''));B=os.path.abspath(os.path.join(A,D)) - if not B.startswith(os.path.abspath(A)):raise web.HTTPForbidden(reason='Invalid path') - return B - async def make_absolute_url(B,rel_path):A=rel_path;A=A.lstrip('/');C=str(B.request.url.with_path(f"/drive/{quote(A)}"));return C - async def entry_details(E,dir_path,entry,parent_rel_path):A=entry;B=os.path.join(dir_path,A);C=os.stat(B);D=os.path.isdir(B);F=_G if D else mimetypes.guess_type(B)[0]or _O;G=C.st_size if not D else _G;H=datetime.fromtimestamp(C.st_ctime).isoformat();I=datetime.fromtimestamp(C.st_mtime).isoformat();J=os.path.join(parent_rel_path,A).replace('\\','/');return{_K:A,_A:_I if D else _D,_L:F,_M:G,'created_at':H,'updated_at':I,_B:await E.make_absolute_url(J)} - async def get(A): - F='page_size';G='page';C=A.request.match_info.get(_J,'');B=await A.get_full_path(C);H=int(A.request.query.get(G,1));D=int(A.request.query.get(F,A.PAGE_SIZE));I=await A.make_absolute_url(C) - if not os.path.exists(B):raise web.HTTPNotFound(reason=_P) - if os.path.isdir(B):E=os.listdir(B);E.sort();J=(H-1)*D;K=J+D;L=E[J:K];M=[await A.entry_details(B,D,C)for D in L];return web.json_response({_F:C,_B:I,'entries':M,'total':len(E),G:H,F:D}) - else: - with open(B,'rb')as N:O=N.read() - P=mimetypes.guess_type(B)[0]or _O;Q={'X-Absolute-Url':I};return web.Response(body=O,content_type=P,headers=Q) - async def post(A): - C='created';D=A.request.match_info.get(_J,'');B=await A.get_full_path(D);E=await A.make_absolute_url(D) - if os.path.exists(B):raise web.HTTPConflict(reason='File or directory already exists') - F=await A.request.post() - if F.get(_A)==_I:os.makedirs(B);return web.json_response({_E:C,_A:_I,_B:E}) - else: - G=F.get(_D) - if not G:raise web.HTTPBadRequest(reason='No file uploaded') - with open(B,'wb')as H:H.write(G.file.read()) - return web.json_response({_E:C,_A:_D,_B:E}) - async def put(A): - C=A.request.match_info.get(_J,'');B=await A.get_full_path(C);D=await A.make_absolute_url(C) - if not os.path.exists(B):raise web.HTTPNotFound(reason='File not found') - if os.path.isdir(B):raise web.HTTPBadRequest(reason='Cannot overwrite directory') - E=await A.request.read() - with open(B,'wb')as F:F.write(E) - return web.json_response({_E:'updated',_B:D}) - async def delete(B): - C='deleted';D=B.request.match_info.get(_J,'');A=await B.get_full_path(D);E=await B.make_absolute_url(D) - if not os.path.exists(A):raise web.HTTPNotFound(reason=_P) - if os.path.isdir(A):os.rmdir(A);return web.json_response({_E:C,_A:_I,_B:E}) - else:os.remove(A);return web.json_response({_E:C,_A:_D,_B:E}) + PAGE_SIZE = 20 + + async def base_path(self): + return await self.services.user.get_home_folder(self.session.get("uid")) + + async def get_full_path(self, rel_path): + base_path = await self.base_path() + safe_path = os.path.normpath(unquote(rel_path or "")) + full_path = os.path.abspath(os.path.join(base_path, safe_path)) + if not full_path.startswith(os.path.abspath(base_path)): + raise web.HTTPForbidden(reason="Invalid path") + return full_path + + async def make_absolute_url(self, rel_path): + rel_path = rel_path.lstrip("/") + url = str(self.request.url.with_path(f"/drive/{quote(rel_path)}")) + return url + + async def entry_details(self, dir_path, entry, parent_rel_path): + entry_path = os.path.join(dir_path, entry) + stat = os.stat(entry_path) + is_dir = os.path.isdir(entry_path) + mimetype = None if is_dir else (mimetypes.guess_type(entry_path)[0] or "application/octet-stream") + size = stat.st_size if not is_dir else None + created_at = datetime.fromtimestamp(stat.st_ctime).isoformat() + updated_at = datetime.fromtimestamp(stat.st_mtime).isoformat() + rel_entry_path = os.path.join(parent_rel_path, entry).replace("\\", "/") + return { + "name": entry, + "type": "dir" if is_dir else "file", + "mimetype": mimetype, + "size": size, + "created_at": created_at, + "updated_at": updated_at, + "absolute_url": await self.make_absolute_url(rel_entry_path), + } + + async def get(self): + rel_path = self.request.match_info.get("rel_path", "") + full_path = await self.get_full_path(rel_path) + page = int(self.request.query.get("page", 1)) + page_size = int(self.request.query.get("page_size", self.PAGE_SIZE)) + abs_url = await self.make_absolute_url(rel_path) + + if not os.path.exists(full_path): + raise web.HTTPNotFound(reason="Path not found") + + if os.path.isdir(full_path): + entries = os.listdir(full_path) + entries.sort() + start = (page - 1) * page_size + end = start + page_size + paged_entries = entries[start:end] + details = [await self.entry_details(full_path, entry, rel_path) for entry in paged_entries] + return web.json_response({ + "path": rel_path, + "absolute_url": abs_url, + "entries": details, + "total": len(entries), + "page": page, + "page_size": page_size, + }) + else: + with open(full_path, "rb") as f: + content = f.read() + mimetype = mimetypes.guess_type(full_path)[0] or "application/octet-stream" + headers = {"X-Absolute-Url": abs_url} + return web.Response(body=content, content_type=mimetype, headers=headers) + + async def post(self): + rel_path = self.request.match_info.get("rel_path", "") + full_path = await self.get_full_path(rel_path) + abs_url = await self.make_absolute_url(rel_path) + if os.path.exists(full_path): + raise web.HTTPConflict(reason="File or directory already exists") + data = await self.request.post() + if data.get("type") == "dir": + os.makedirs(full_path) + return web.json_response({"status": "created", "type": "dir", "absolute_url": abs_url}) + else: + file_field = data.get("file") + if not file_field: + raise web.HTTPBadRequest(reason="No file uploaded") + with open(full_path, "wb") as f: + f.write(file_field.file.read()) + return web.json_response({"status": "created", "type": "file", "absolute_url": abs_url}) + + async def put(self): + rel_path = self.request.match_info.get("rel_path", "") + full_path = await self.get_full_path(rel_path) + abs_url = await self.make_absolute_url(rel_path) + if not os.path.exists(full_path): + raise web.HTTPNotFound(reason="File not found") + if os.path.isdir(full_path): + raise web.HTTPBadRequest(reason="Cannot overwrite directory") + body = await self.request.read() + with open(full_path, "wb") as f: + f.write(body) + return web.json_response({"status": "updated", "absolute_url": abs_url}) + + async def delete(self): + rel_path = self.request.match_info.get("rel_path", "") + full_path = await self.get_full_path(rel_path) + abs_url = await self.make_absolute_url(rel_path) + if not os.path.exists(full_path): + raise web.HTTPNotFound(reason="Path not found") + if os.path.isdir(full_path): + os.rmdir(full_path) + return web.json_response({"status": "deleted", "type": "dir", "absolute_url": abs_url}) + else: + os.remove(full_path) + return web.json_response({"status": "deleted", "type": "file", "absolute_url": abs_url}) + + class DriveViewi2(BaseView): - login_required=True - async def get(A): - G='/drive.bin/';D=A.request.match_info.get('drive');H=A.request.query.get('before');E={} - if H:E['created_at__lt']=H - if D: - E['drive_uid']=D;F=await A.services.drive.get(uid=D);I=[] - async for C in A.services.drive_item.find(**E):B=C.record;B[_H]=G+B[_C]+'.'+C.extension;I.append(B) - return web.json_response(I) - L=await A.services.user.get(uid=A.session.get(_C));J=[] - async for F in A.services.drive.get_by_user(L[_C]): - B=F.record;B[_N]=[] - async for C in F.items:K=C.record;K[_H]=G+K[_C]+'.'+C.extension;B[_N].append(C.record) - J.append(B) - return web.json_response(J) \ No newline at end of file + + login_required = True + + async def get(self): + + drive_uid = self.request.match_info.get("drive") + + + before = self.request.query.get("before") + filters = {} + if before: + filters["created_at__lt"] = before + + if drive_uid: + filters['drive_uid'] = drive_uid + drive = await self.services.drive.get(uid=drive_uid) + drive_items = [] + + + + async for item in self.services.drive_item.find(**filters): + record = item.record + record["url"] = "/drive.bin/" + record["uid"] + "." + item.extension + drive_items.append(record) + return web.json_response(drive_items) + + user = await self.services.user.get(uid=self.session.get("uid")) + + drives = [] + async for drive in self.services.drive.get_by_user(user["uid"]): + record = drive.record + record["items"] = [] + async for item in drive.items: + drive_item_record = item.record + drive_item_record["url"] = ( + "/drive.bin/" + drive_item_record["uid"] + "." + item.extension + ) + record["items"].append(item.record) + drives.append(record) + + return web.json_response(drives) diff --git a/src/snek/view/index.py b/src/snek/view/index.py index 2bd3245..2f44443 100644 --- a/src/snek/view/index.py +++ b/src/snek/view/index.py @@ -1,6 +1,23 @@ +# Written by retoor@molodetz.nl + + +# This code defines an asynchronous IndexView class inheriting from BaseView with a method to render an HTML template. + + +# External imports: BaseView from snek.system.view + + +# MIT License + + from aiohttp import web + from snek.system.view import BaseView + + class IndexView(BaseView): - async def get(A): - if A.session.get('uid'):return web.HTTPFound('/web.html') - return await A.render_template('index.html') \ No newline at end of file + async def get(self): + if self.session.get("uid"): + return web.HTTPFound("/web.html") + + return await self.render_template("index.html") diff --git a/src/snek/view/login.py b/src/snek/view/login.py index 849a8e1..fe8cf4d 100644 --- a/src/snek/view/login.py +++ b/src/snek/view/login.py @@ -1,15 +1,44 @@ -_B='/web.html' -_A='logged_in' +# Written by retoor@molodetz.nl + +# This source code defines a LoginView class that inherits from BaseFormView and handles user authentication. It checks if a user is logged in, provides a JSON response or renders a login HTML template as needed, and processes form submissions to authenticate users. + +# The code imports the LoginForm from snek.form.login and BaseFormView from snek.system.view, both of which are likely custom modules in the system, as well as web from aiohttp for handling HTTP responses. + +# MIT License + from aiohttp import web + from snek.form.login import LoginForm from snek.system.view import BaseFormView + + class LoginView(BaseFormView): - form=LoginForm;login_required=False - async def get(A): - if A.session.get(_A):return web.HTTPFound(_B) - if A.request.path.endswith('.json'):return await super().get() - return await A.render_template('login.html',{'form':await A.form(app=A.app).to_json()}) - async def submit(B,form): - D='color';E='uid';C='username' - if await form.is_valid:A=await B.services.user.get(username=form[C],deleted_at=None);await B.services.user.save(A);B.session.update({_A:True,C:A[C],E:A[E],D:A[D]});return{'redirect_url':_B} - return{'is_valid':False} \ No newline at end of file + form = LoginForm + + login_required = False + + async def get(self): + if self.session.get("logged_in"): + return web.HTTPFound("/web.html") + if self.request.path.endswith(".json"): + return await super().get() + return await self.render_template( + "login.html", {"form": await self.form(app=self.app).to_json()} + ) + + async def submit(self, form): + if await form.is_valid: + user = await self.services.user.get( + username=form["username"], deleted_at=None + ) + await self.services.user.save(user) + self.session.update( + { + "logged_in": True, + "username": user["username"], + "uid": user["uid"], + "color": user["color"], + } + ) + return {"redirect_url": "/web.html"} + return {"is_valid": False} diff --git a/src/snek/view/login_form.py b/src/snek/view/login_form.py index 39b5be3..acf7c75 100644 --- a/src/snek/view/login_form.py +++ b/src/snek/view/login_form.py @@ -1,8 +1,23 @@ +# Written by retoor@molodetz.nl + +# This code defines an asynchronous view for handling a login form. It checks if the form is valid, sets session variables for a logged-in user, and provides a redirect URL if successful. + +# Imports: LoginForm from snek.form.login and BaseFormView from snek.system.view + +# MIT License + + from snek.form.login import LoginForm from snek.system.view import BaseFormView + + class LoginFormView(BaseFormView): - form=LoginForm - async def submit(A,form): - B=form - if await B.is_valid():A.session['logged_in']=True;A.session['username']=B.username.value;A.session['uid']=B.uid.value;return{'redirect_url':'/web.html'} - return{'is_valid':False} \ No newline at end of file + form = LoginForm + + async def submit(self, form): + if await form.is_valid(): + self.session["logged_in"] = True + self.session["username"] = form.username.value + self.session["uid"] = form.uid.value + return {"redirect_url": "/web.html"} + return {"is_valid": False} diff --git a/src/snek/view/logout.py b/src/snek/view/logout.py index 5594774..42016d8 100644 --- a/src/snek/view/logout.py +++ b/src/snek/view/logout.py @@ -1,14 +1,56 @@ -_B='username' -_A='logged_in' +# Written by retoor@molodetz.nl + + +# This code provides a view for logging out users. It handles GET and POST requests, deletes session information, and redirects the user after logging out. + + +# This code imports 'web' from the 'aiohttp' library to handle HTTP operations and imports 'BaseView' from 'snek.system.view' to extend a base class for creating views. + + +# 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 aiohttp import web + from snek.system.view import BaseView + + class LogoutView(BaseView): - redirect_url='/';login_required=True - async def get(A): - try:del A.session[_A];del A.session['uid'];del A.session[_B] - except KeyError:pass - return web.HTTPFound(A.redirect_url) - async def post(A): - try:del A.session[_A];del A.session['uid'];del A.session[_B] - except KeyError:pass - return await A.json_response({'redirect_url':A.redirect_url}) \ No newline at end of file + redirect_url = "/" + login_required = True + + async def get(self): + try: + del self.session["logged_in"] + del self.session["uid"] + del self.session["username"] + except KeyError: + pass + return web.HTTPFound(self.redirect_url) + + async def post(self): + try: + del self.session["logged_in"] + del self.session["uid"] + del self.session["username"] + except KeyError: + pass + return await self.json_response({"redirect_url": self.redirect_url}) diff --git a/src/snek/view/register.py b/src/snek/view/register.py index ba48820..96eed8a 100644 --- a/src/snek/view/register.py +++ b/src/snek/view/register.py @@ -1,12 +1,41 @@ -_B='/web.html' -_A='logged_in' +# Written by retoor@molodetz.nl + +# This module defines a web view for user registration. It handles GET requests and form submissions for the registration process. + +# The code makes use of 'RegisterForm' from 'snek.form.register' for handling registration forms and 'BaseFormView' from 'snek.system.view' for basic view functionalities. + +# MIT License + from aiohttp import web + from snek.form.register import RegisterForm from snek.system.view import BaseFormView + + class RegisterView(BaseFormView): - form=RegisterForm;login_required=False - async def get(A): - if A.session.get(_A):return web.HTTPFound(_B) - if A.request.path.endswith('.json'):return await super().get() - return await A.render_template('register.html',{'form':await A.form(app=A.app).to_json()}) - async def submit(C,form):D='color';E='username';F='uid';A=form;B=await C.app.services.user.register(A.email.value,A.username.value,A.password.value);C.request.session.update({F:B[F],E:B[E],_A:True,D:B[D]});return{'redirect_url':_B} \ No newline at end of file + form = RegisterForm + + login_required = False + + async def get(self): + if self.session.get("logged_in"): + return web.HTTPFound("/web.html") + if self.request.path.endswith(".json"): + return await super().get() + return await self.render_template( + "register.html", {"form": await self.form(app=self.app).to_json()} + ) + + async def submit(self, form): + result = await self.app.services.user.register( + form.email.value, form.username.value, form.password.value + ) + self.request.session.update( + { + "uid": result["uid"], + "username": result["username"], + "logged_in": True, + "color": result["color"], + } + ) + return {"redirect_url": "/web.html"} diff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py index cf5dbbb..7b98647 100644 --- a/src/snek/view/register_form.py +++ b/src/snek/view/register_form.py @@ -1,5 +1,47 @@ +# Written by retoor@molodetz.nl + +# This code defines a `RegisterFormView` class that handles the user registration process by using a form object, a view parent class, and asynchronously submitting the form data to register a user. It then stores the user's session details and provides a redirect URL to a specific page. + +# Imports used but not part of the language: +# snek.form.register.RegisterForm, snek.system.view.BaseFormView + +# 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.form.register import RegisterForm from snek.system.view import BaseFormView + + class RegisterFormView(BaseFormView): - form=RegisterForm - async def submit(C,form):D='color';E='username';F='uid';A=form;B=await C.app.services.user.register(A.email.value,A.username.value,A.password.value);C.request.session.update({F:B[F],E:B[E],'logged_in':True,D:B[D]});return{'redirect_url':'/web.html'} \ No newline at end of file + form = RegisterForm + + async def submit(self, form): + result = await self.app.services.user.register( + form.email.value, form.username.value, form.password.value + ) + self.request.session.update( + { + "uid": result["uid"], + "username": result["username"], + "logged_in": True, + "color": result["color"], + } + ) + return {"redirect_url": "/web.html"} diff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py index 1896ba8..3161f49 100644 --- a/src/snek/view/rpc.py +++ b/src/snek/view/rpc.py @@ -1,105 +1,283 @@ -_M='noresponse' -_L='deleted_at' -_K='Not allowed' -_J='password' -_I='logged_in' -_H='channel_uid' -_G='last_ping' -_F='nick' -_E=None -_D=True -_C=False -_B='username' -_A='uid' -import json,traceback +# Written by retoor@molodetz.nl + +# This source code implements a WebSocket-based RPC (Remote Procedure Call) view that uses asynchronous methods to facilitate real-time communication and services for an authenticated user session in a web application. The class handles WebSocket events, user authentication, and various RPC interactions such as login, message retrieval, and more. + +# External imports are used from the aiohttp library for the WebSocket response handling and the snek.system view for the BaseView class. + +# 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. + + +import json +import traceback + from aiohttp import web + from snek.system.model import now from snek.system.profiler import Profiler from snek.system.view import BaseView + + class RPCView(BaseView): - class RPCApi: - def __init__(A,view,ws):A.view=view;A.app=A.view.app;A.services=A.app.services;A.ws=ws - @property - def user_uid(self):return self.view.session.get(_A) - @property - def request(self):return self.view.request - def _require_login(A): - if not A.is_logged_in:raise Exception('Not logged in') - @property - def is_logged_in(self):return self.view.session.get(_I,_C) - async def mark_as_read(A,channel_uid):A._require_login();await A.services.channel_member.mark_as_read(channel_uid,A.user_uid);return _D - async def login(A,username,password): - D=username;E=await A.services.user.validate_login(D,password) - if not E:raise Exception('Invalid username or password') - B=await A.services.user.get(username=D);A.view.session[_A]=B[_A];A.view.session[_I]=_D;A.view.session[_B]=B[_B];A.view.session['user_nick']=B[_F];C=B.record;del C[_J];del C[_L];await A.services.socket.add(A.ws,A.view.request.session.get(_A)) - async for F in A.services.channel_member.find(user_uid=A.view.request.session.get(_A),deleted_at=_E,is_banned=_C):await A.services.socket.subscribe(A.ws,F[_H],A.view.request.session.get(_A)) - return C - async def search_user(A,query):A._require_login();return[A[_B]for A in await A.services.user.search(query)] - async def get_user(C,user_uid): - A=user_uid;C._require_login() - if not A:A=C.user_uid - D=await C.services.user.get(uid=A);B=D.record;del B[_J];del B[_L] - if A!=D[_A]:del B['email'] - return B - async def get_messages(A,channel_uid,offset=0,timestamp=_E): - A._require_login();B=[] - for C in await A.services.channel_message.offset(channel_uid,offset or 0,timestamp or _E):D=await A.services.channel_message.to_extended_dict(C);B.append(D) - return B - async def get_channels(B): - D='is_read_only';E='is_moderator';F='tag';G='color';C='new_count';B._require_login();H=[] - async for A in B.services.channel_member.find(user_uid=B.user_uid,is_banned=_C): - I=await B.services.channel.get(uid=A[_H]);J=await I.get_last_message();K=_E - if J:L=await J.get_user();K=L[G] - H.append({'name':A['label'],_A:A[_H],F:I[F],C:A[C],E:A[E],D:A[D],C:A[C],G:K}) - return H - async def send_message(A,channel_uid,message):A._require_login();await A.services.chat.send(A.user_uid,channel_uid,message);return _D - async def echo(A,*B):A._require_login();return B - async def query(B,*C): - B._require_login();E=C[0];D=E.lower() - if any(A in D for A in['drop','alter','update','delete','replace','insert','truncate'])and'select'not in D:raise Exception(_K) - F=[dict(A)async for A in B.services.channel.query(C[0])] - for A in F: - try:del A['email'] - except KeyError:pass - try:del A[_J] - except KeyError:pass - try:del A['message'] - except:pass - try:del A['html'] - except:pass - return[dict(A)async for A in B.services.channel.query(C[0])] - async def __call__(A,data): - I='success';E='data';F=data;B='callId' - try: - G=F.get(B);C=F.get('method') - if C.startswith('_'):raise Exception(_K) - L=F.get('args')or[] - if hasattr(super(),C)or not hasattr(A,C):return await A._send_json({B:G,E:_K}) - J=getattr(A,C.replace('.','_'),_E) - if not J:raise Exception('Method not found') - K=_D - try:H=await J(*L) - except Exception as D:H={'exception':str(D),'traceback':traceback.format_exc()};K=_C - if H!=_M:await A._send_json({B:G,I:K,E:H}) - except Exception as D:print(str(D),flush=_D);await A._send_json({B:G,I:_C,E:str(D)}) - async def _send_json(A,obj):await A.ws.send_str(json.dumps(obj,default=str)) - async def get_online_users(A,channel_uid):A._require_login();return[{_A:A[_A],_B:A[_B],_F:A[_F],_G:A[_G]}async for A in A.services.channel.get_online_users(channel_uid)] - async def echo(A,obj):await A.ws.send_json(obj);return _M - async def get_users(A,channel_uid):A._require_login();return[{_A:A[_A],_B:A[_B],_F:A[_F],_G:A[_G]}async for A in A.services.channel.get_users(channel_uid)] - async def ping(A,callId,*C): - if A.user_uid:B=await A.services.user.get(uid=A.user_uid);B[_G]=now();await A.services.user.save(B) - return{'pong':C} - async def get(A): - B=web.WebSocketResponse();await B.prepare(A.request) - if A.request.session.get(_I): - await A.services.socket.add(B,A.request.session.get(_A)) - async for D in A.services.channel_member.find(user_uid=A.request.session.get(_A),deleted_at=_E,is_banned=_C):await A.services.socket.subscribe(B,D[_H],A.request.session.get(_A)) - E=RPCView.RPCApi(A,B) - async for C in B: - if C.type==web.WSMsgType.TEXT: - try: - async with Profiler():await E(C.json()) - except Exception as F:print('Deleting socket',F,flush=_D);await A.services.socket.delete(B);break - elif C.type==web.WSMsgType.ERROR:0 - elif C.type==web.WSMsgType.CLOSE:0 - return B \ No newline at end of file + + class RPCApi: + def __init__(self, view, ws): + self.view = view + self.app = self.view.app + self.services = self.app.services + self.ws = ws + + @property + def user_uid(self): + return self.view.session.get("uid") + + @property + def request(self): + return self.view.request + + def _require_login(self): + if not self.is_logged_in: + raise Exception("Not logged in") + + @property + def is_logged_in(self): + return self.view.session.get("logged_in", False) + + async def mark_as_read(self, channel_uid): + self._require_login() + await self.services.channel_member.mark_as_read(channel_uid, self.user_uid) + return True + + async def login(self, username, password): + success = await self.services.user.validate_login(username, password) + if not success: + raise Exception("Invalid username or password") + user = await self.services.user.get(username=username) + self.view.session["uid"] = user["uid"] + self.view.session["logged_in"] = True + self.view.session["username"] = user["username"] + self.view.session["user_nick"] = user["nick"] + record = user.record + del record["password"] + del record["deleted_at"] + await self.services.socket.add( + self.ws, self.view.request.session.get("uid") + ) + async for subscription in self.services.channel_member.find( + user_uid=self.view.request.session.get("uid"), + deleted_at=None, + is_banned=False, + ): + await self.services.socket.subscribe( + self.ws, + subscription["channel_uid"], + self.view.request.session.get("uid"), + ) + return record + + async def search_user(self, query): + self._require_login() + return [user["username"] for user in await self.services.user.search(query)] + + async def get_user(self, user_uid): + self._require_login() + if not user_uid: + user_uid = self.user_uid + user = await self.services.user.get(uid=user_uid) + record = user.record + del record["password"] + del record["deleted_at"] + if user_uid != user["uid"]: + del record["email"] + return record + + async def get_messages(self, channel_uid, offset=0, timestamp=None): + self._require_login() + messages = [] + for message in await self.services.channel_message.offset( + channel_uid, offset or 0, timestamp or None + ): + extended_dict = await self.services.channel_message.to_extended_dict( + message + ) + messages.append(extended_dict) + return messages + + async def get_channels(self): + self._require_login() + channels = [] + async for subscription in self.services.channel_member.find( + user_uid=self.user_uid, is_banned=False + ): + channel = await self.services.channel.get( + uid=subscription["channel_uid"] + ) + last_message = await channel.get_last_message() + color = None + if last_message: + last_message_user = await last_message.get_user() + color = last_message_user["color"] + channels.append( + { + "name": subscription["label"], + "uid": subscription["channel_uid"], + "tag": channel["tag"], + "new_count": subscription["new_count"], + "is_moderator": subscription["is_moderator"], + "is_read_only": subscription["is_read_only"], + "new_count": subscription["new_count"], + "color": color, + } + ) + return channels + + async def send_message(self, channel_uid, message): + self._require_login() + await self.services.chat.send(self.user_uid, channel_uid, message) + return True + + async def echo(self, *args): + self._require_login() + return args + + async def query(self, *args): + self._require_login() + query = args[0] + lowercase = query.lower() + if ( + any( + keyword in lowercase + for keyword in [ + "drop", + "alter", + "update", + "delete", + "replace", + "insert", + "truncate", + ] + ) + and "select" not in lowercase + ): + raise Exception("Not allowed") + records = [ + dict(record) async for record in self.services.channel.query(args[0]) + ] + for record in records: + try: + del record["email"] + except KeyError: + pass + try: + del record["password"] + except KeyError: + pass + try: + del record["message"] + except: + pass + try: + del record["html"] + except: + pass + return [ + dict(record) async for record in self.services.channel.query(args[0]) + ] + + async def __call__(self, data): + try: + call_id = data.get("callId") + method_name = data.get("method") + if method_name.startswith("_"): + raise Exception("Not allowed") + args = data.get("args") or [] + if hasattr(super(), method_name) or not hasattr(self, method_name): + return await self._send_json( + {"callId": call_id, "data": "Not allowed"} + ) + method = getattr(self, method_name.replace(".", "_"), None) + if not method: + raise Exception("Method not found") + success = True + try: + result = await method(*args) + except Exception as ex: + result = {"exception": str(ex), "traceback": traceback.format_exc()} + success = False + if result != "noresponse": + await self._send_json( + {"callId": call_id, "success": success, "data": result} + ) + except Exception as ex: + print(str(ex), flush=True) + await self._send_json( + {"callId": call_id, "success": False, "data": str(ex)} + ) + + async def _send_json(self, obj): + await self.ws.send_str(json.dumps(obj, default=str)) + + async def get_online_users(self, channel_uid): + self._require_login() + + return [ + { + "uid": record["uid"], + "username": record["username"], + "nick": record["nick"], + "last_ping": record["last_ping"], + } + async for record in self.services.channel.get_online_users(channel_uid) + ] + + async def echo(self, obj): + await self.ws.send_json(obj) + return "noresponse" + + async def get_users(self, channel_uid): + self._require_login() + + return [ + { + "uid": record["uid"], + "username": record["username"], + "nick": record["nick"], + "last_ping": record["last_ping"], + } + async for record in self.services.channel.get_users(channel_uid) + ] + + async def ping(self, callId, *args): + if self.user_uid: + user = await self.services.user.get(uid=self.user_uid) + user["last_ping"] = now() + await self.services.user.save(user) + return {"pong": args} + + async def get(self): + ws = web.WebSocketResponse() + await ws.prepare(self.request) + if self.request.session.get("logged_in"): + await self.services.socket.add(ws, self.request.session.get("uid")) + async for subscription in self.services.channel_member.find( + user_uid=self.request.session.get("uid"), + deleted_at=None, + is_banned=False, + ): + await self.services.socket.subscribe( + ws, subscription["channel_uid"], self.request.session.get("uid") + ) + rpc = RPCView.RPCApi(self, ws) + async for msg in ws: + if msg.type == web.WSMsgType.TEXT: + try: + async with Profiler(): + await rpc(msg.json()) + except Exception as ex: + print("Deleting socket", ex, flush=True) + await self.services.socket.delete(ws) + break + elif msg.type == web.WSMsgType.ERROR: + pass + elif msg.type == web.WSMsgType.CLOSE: + pass + return ws diff --git a/src/snek/view/search_user.py b/src/snek/view/search_user.py index 5e5b7e2..1f09a26 100644 --- a/src/snek/view/search_user.py +++ b/src/snek/view/search_user.py @@ -1,12 +1,56 @@ +# Written by retoor@molodetz.nl + +# This code implements a web view feature for searching users. It handles GET requests to retrieve user data based on a search query and also processes form submissions. + +# Imports used that are not part of the Python language: +# - aiohttp: Used to handle asynchronous web server functionalities. +# - snek.form.search_user: Contains the definition for SearchUserForm, a form specific to searching users. +# - snek.system.view: Provides the BaseFormView class to facilitate form-related operations in a web context. + +# 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.form.search_user import SearchUserForm from snek.system.view import BaseFormView + + class SearchUserView(BaseFormView): - form=SearchUserForm;login_required=True - async def get(A): - C='query';D=[];B=A.request.query.get(C) - if B:D=[A.record for A in await A.app.services.user.search(B)] - if A.request.path.endswith('.json'):return await super().get() - E=await A.app.services.user.get(uid=A.session.get('uid'));return await A.render_template('search_user.html',{'users':D,C:B or'','current_user':E}) - async def submit(A,form): - if await form.is_valid:return{'redirect_url':'/search-user.html?query='+form['username']} - return{'is_valid':False} \ No newline at end of file + form = SearchUserForm + login_required = True + + async def get(self): + users = [] + query = self.request.query.get("query") + if query: + users = [user.record for user in await self.app.services.user.search(query)] + + if self.request.path.endswith(".json"): + return await super().get() + current_user = await self.app.services.user.get(uid=self.session.get("uid")) + return await self.render_template( + "search_user.html", + {"users": users, "query": query or "", "current_user": current_user}, + ) + + async def submit(self, form): + if await form.is_valid: + return {"redirect_url": "/search-user.html?query=" + form["username"]} + return {"is_valid": False} diff --git a/src/snek/view/settings/index.py b/src/snek/view/settings/index.py index fc58857..418ef3d 100644 --- a/src/snek/view/settings/index.py +++ b/src/snek/view/settings/index.py @@ -1,4 +1,9 @@ from snek.system.view import BaseView + + class SettingsIndexView(BaseView): - login_required=True - async def get(A):return await A.render_template('settings/index.html') \ No newline at end of file + + login_required = True + + async def get(self): + return await self.render_template("settings/index.html") diff --git a/src/snek/view/settings/profile.py b/src/snek/view/settings/profile.py index 4a3f897..164c526 100644 --- a/src/snek/view/settings/profile.py +++ b/src/snek/view/settings/profile.py @@ -1,13 +1,38 @@ -_C='profile' -_B='uid' -_A='nick' from aiohttp import web + from snek.form.settings.profile import SettingsProfileForm from snek.system.view import BaseFormView + + class SettingsProfileView(BaseFormView): - form=SettingsProfileForm;login_required=True - async def get(A): - C='user';B=A.form(app=A.app) - if A.request.path.endswith('.json'):B[_A]=A.request[C][_A];return web.json_response(await B.to_json()) - D=await A.services.user_property.get(A.session.get(_B),_C);E=await A.services.user.get(uid=A.session.get(_B));return await A.render_template('settings/profile.html',{'form':await B.to_json(),C:E,_C:D or''}) - async def post(A):C=await A.request.post();B=await A.services.user.get(uid=A.session.get(_B));B[_A]=C[_A];await A.services.user.save(B);await A.services.user_property.set(B[_B],_C,C[_C]);return web.HTTPFound('/settings/profile.html') \ No newline at end of file + form = SettingsProfileForm + + login_required = True + + async def get(self): + form = self.form(app=self.app) + + if self.request.path.endswith(".json"): + form["nick"] = self.request["user"]["nick"] + + return web.json_response(await form.to_json()) + + profile = await self.services.user_property.get( + self.session.get("uid"), "profile" + ) + + user = await self.services.user.get(uid=self.session.get("uid")) + + return await self.render_template( + "settings/profile.html", + {"form": await form.to_json(), "user": user, "profile": profile or ""}, + ) + + async def post(self): + data = await self.request.post() + user = await self.services.user.get(uid=self.session.get("uid")) + + user["nick"] = data["nick"] + await self.services.user.save(user) + await self.services.user_property.set(user["uid"], "profile", data["profile"]) + return web.HTTPFound("/settings/profile.html") diff --git a/src/snek/view/settings/repositories.py b/src/snek/view/settings/repositories.py index 1cf96fd..093d229 100644 --- a/src/snek/view/settings/repositories.py +++ b/src/snek/view/settings/repositories.py @@ -1,37 +1,86 @@ -_F='repository' -_E='/settings/repositories/index.html' -_D='is_private' -_C=True -_B='name' -_A='uid' import asyncio from aiohttp import web + from snek.system.view import BaseFormView import pathlib + class RepositoriesIndexView(BaseFormView): - login_required=_C - async def get(A): - C=A.session.get(_A);B=[] - async for D in A.services.repository.find(user_uid=C):B.append(D.record) - E=await A.services.user.get(uid=A.session.get(_A));return await A.render_template('settings/repositories/index.html',{'repositories':B,'user':E}) + + login_required = True + + async def get(self): + + user_uid = self.session.get("uid") + + repositories = [] + async for repository in self.services.repository.find(user_uid=user_uid): + repositories.append(repository.record) + + user = await self.services.user.get(uid=self.session.get("uid")) + + return await self.render_template("settings/repositories/index.html", {"repositories": repositories, "user": user}) + + + + class RepositoriesCreateView(BaseFormView): - login_required=_C - async def get(A):return await A.render_template('settings/repositories/create.html') - async def post(A):B=await A.request.post();C=await A.services.repository.create(user_uid=A.session.get(_A),name=B[_B],is_private=int(B.get(_D,0)));return web.HTTPFound(_E) + + login_required = True + + async def get(self): + + return await self.render_template("settings/repositories/create.html") + + async def post(self): + data = await self.request.post() + repository = await self.services.repository.create(user_uid=self.session.get("uid"), name=data['name'], is_private=int(data.get('is_private',0))) + return web.HTTPFound("/settings/repositories/index.html") + class RepositoriesUpdateView(BaseFormView): - login_required=_C - async def get(A): - B=await A.services.repository.get(user_uid=A.session.get(_A),name=A.request.match_info[_B]) - if not B:return web.HTTPNotFound() - return await A.render_template('settings/repositories/update.html',{_F:B.record}) - async def post(A):C=await A.request.post();B=await A.services.repository.get(user_uid=A.session.get(_A),name=A.request.match_info[_B]);B[_D]=int(C.get(_D,0));await A.services.repository.save(B);return web.HTTPFound(_E) + + login_required = True + + async def get(self): + + repository = await self.services.repository.get( + user_uid=self.session.get("uid"), name=self.request.match_info["name"] + ) + if not repository: + return web.HTTPNotFound() + return await self.render_template("settings/repositories/update.html", {"repository": repository.record}) + + async def post(self): + data = await self.request.post() + repository = await self.services.repository.get( + user_uid=self.session.get("uid"), name=self.request.match_info["name"] + ) + repository['is_private'] = int(data.get('is_private',0)) + await self.services.repository.save(repository) + return web.HTTPFound("/settings/repositories/index.html") + class RepositoriesDeleteView(BaseFormView): - login_required=_C - async def get(A): - B=await A.services.repository.get(user_uid=A.session.get(_A),name=A.request.match_info[_B]) - if not B:return web.HTTPNotFound() - return await A.render_template('settings/repositories/delete.html',{_F:B.record}) - async def post(A): - B=A.session.get(_A);C=A.request.match_info[_B];D=await A.services.repository.get(user_uid=B,name=C) - if not D:return web.HTTPNotFound() - await A.services.repository.delete(user_uid=B,name=C);return web.HTTPFound(_E) \ No newline at end of file + + login_required = True + + async def get(self): + + repository = await self.services.repository.get( + user_uid=self.session.get("uid"), name=self.request.match_info["name"] + ) + if not repository: + return web.HTTPNotFound() + + return await self.render_template("settings/repositories/delete.html", {"repository": repository.record}) + + async def post(self): + user_uid = self.session.get("uid") + name = self.request.match_info["name"] + repository = await self.services.repository.get( + user_uid=user_uid, name=name + ) + if not repository: + return web.HTTPNotFound() + await self.services.repository.delete(user_uid=user_uid, name=name) + return web.HTTPFound("/settings/repositories/index.html") + + diff --git a/src/snek/view/stats.py b/src/snek/view/stats.py index dbf7fc6..1680c5c 100644 --- a/src/snek/view/stats.py +++ b/src/snek/view/stats.py @@ -1,5 +1,13 @@ import json + from aiohttp import web + from snek.system.view import BaseView + + class StatsView(BaseView): - async def get(B):A=await B.app.cache.get_stats();A=json.dumps({'total':len(A),'stats':A},default=str,indent=1);return web.Response(text=A,content_type='application/json') \ No newline at end of file + + async def get(self): + data = await self.app.cache.get_stats() + data = json.dumps({"total": len(data), "stats": data}, default=str, indent=1) + return web.Response(text=data, content_type="application/json") diff --git a/src/snek/view/status.py b/src/snek/view/status.py index 672d20f..4675572 100644 --- a/src/snek/view/status.py +++ b/src/snek/view/status.py @@ -1,10 +1,73 @@ +# Written by retoor@molodetz.nl + +# This code defines an async class-based view called StatusView for handling HTTP GET requests. It fetches user details and their associated channel memberships from a database and returns a JSON response with user information if the user is logged in. + +# The code uses an imported module `BaseView`. There are dependencies on the `snek.system.view` module which provides the BaseView class. + +# 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.view import BaseView + + class StatusView(BaseView): - async def get(C): - G='color';H='nick';I='email';J='username';K='is_banned';L='is_muted';M='is_read_only';N='is_moderator';O='user_uid';P='description';E='channel_uid';D='uid';Q=[];A={};F=C.session.get(D) - if F: - A=await C.app.services.user.get(uid=F) - if not A:return await C.json_response({'error':'User not found'},status=404) - async for B in C.app.services.channel_member.find(user_uid=F,deleted_at=None,is_banned=False):R=await C.app.services.channel.get(uid=B[E]);Q.append({'name':R['label'],P:B[P],O:B[O],N:B[N],M:B[M],L:B[L],K:B[K],E:B[E],D:B[D]}) - A={J:A[J],I:A[I],H:A[H],D:A[D],G:A[G],'memberships':Q} - return await C.json_response({'user':A,'cache':await C.app.cache.create_cache_key(C.app.cache.cache,None)}) \ No newline at end of file + async def get(self): + memberships = [] + user = {} + + user_id = self.session.get("uid") + if user_id: + user = await self.app.services.user.get(uid=user_id) + if not user: + return await self.json_response({"error": "User not found"}, status=404) + + async for model in self.app.services.channel_member.find( + user_uid=user_id, deleted_at=None, is_banned=False + ): + channel = await self.app.services.channel.get(uid=model["channel_uid"]) + memberships.append( + { + "name": channel["label"], + "description": model["description"], + "user_uid": model["user_uid"], + "is_moderator": model["is_moderator"], + "is_read_only": model["is_read_only"], + "is_muted": model["is_muted"], + "is_banned": model["is_banned"], + "channel_uid": model["channel_uid"], + "uid": model["uid"], + } + ) + user = { + "username": user["username"], + "email": user["email"], + "nick": user["nick"], + "uid": user["uid"], + "color": user["color"], + "memberships": memberships, + } + + return await self.json_response( + { + "user": user, + "cache": await self.app.cache.create_cache_key( + self.app.cache.cache, None + ), + } + ) diff --git a/src/snek/view/terminal.py b/src/snek/view/terminal.py index 43c1fd1..d3af9b0 100644 --- a/src/snek/view/terminal.py +++ b/src/snek/view/terminal.py @@ -1,23 +1,54 @@ -_B=True -_A='uid' -import pathlib,aiohttp +import pathlib + +import aiohttp + from snek.system.terminal import TerminalSession from snek.system.view import BaseView + + class TerminalSocketView(BaseView): - login_required=_B;user_sessions={} - async def prepare_drive(C): - D=await C.services.user.get(uid=C.session.get(_A));A=pathlib.Path('drive').joinpath(D[_A]);A.mkdir(parents=_B,exist_ok=_B);E=pathlib.Path('terminal') - for B in E.iterdir(): - F=A.joinpath(B.name) - if not B.is_dir():F.write_bytes(B.read_bytes()) - return A - async def get(A): - B=aiohttp.web.WebSocketResponse();await B.prepare(A.request);D=await A.services.user.get(uid=A.session.get(_A));F=await A.prepare_drive();G=f"docker run -v ./{F}/:/root -it --memory 512M --cpus=0.5 -w /root snek_ubuntu /bin/bash";C=A.user_sessions.get(D[_A]) - if not C:A.user_sessions[D[_A]]=TerminalSession(command=G) - C=A.user_sessions[D[_A]];await C.add_websocket(B) - async for E in B: - if E.type==aiohttp.WSMsgType.BINARY:await C.write_input(E.data.decode()) - return B + + login_required = True + + user_sessions = {} + + async def prepare_drive(self): + user = await self.services.user.get(uid=self.session.get("uid")) + root = pathlib.Path("drive").joinpath(user["uid"]) + root.mkdir(parents=True, exist_ok=True) + terminal_folder = pathlib.Path("terminal") + for path in terminal_folder.iterdir(): + destination_path = root.joinpath(path.name) + if not path.is_dir(): + destination_path.write_bytes(path.read_bytes()) + return root + + async def get(self): + + ws = aiohttp.web.WebSocketResponse() + await ws.prepare(self.request) + user = await self.services.user.get(uid=self.session.get("uid")) + root = await self.prepare_drive() + + command = f"docker run -v ./{root}/:/root -it --memory 512M --cpus=0.5 -w /root snek_ubuntu /bin/bash" + + session = self.user_sessions.get(user["uid"]) + if not session: + self.user_sessions[user["uid"]] = TerminalSession(command=command) + session = self.user_sessions[user["uid"]] + await session.add_websocket(ws) + # asyncio.create_task(session.read_output(ws)) + + async for msg in ws: + if msg.type == aiohttp.WSMsgType.BINARY: + await session.write_input(msg.data.decode()) + + return ws + + class TerminalView(BaseView): - login_required=_B - async def get(A):return await A.request.app.render_template('terminal.html',A.request) \ No newline at end of file + + login_required = True + + async def get(self): + return await self.request.app.render_template("terminal.html", self.request) diff --git a/src/snek/view/threads.py b/src/snek/view/threads.py index 3b7425d..bc923c6 100644 --- a/src/snek/view/threads.py +++ b/src/snek/view/threads.py @@ -1,11 +1,37 @@ from snek.system.view import BaseView + + class ThreadsView(BaseView): - async def get(B): - I='color';J='user_uid';K='name_color';L='new_count';F='uid';C='last_message_on';G=[];M=await B.services.user.get(uid=B.session.get(F)) - async for H in M.get_channel_members(): - A={};D=await B.services.channel.get(uid=H['channel_uid']);E=await D.get_last_message() - if not E:continue - A[F]=D[F];A['name']=await H.get_name();A[L]=H[L];A[C]=D[C];A['created_at']=A[C];A[K]='#f05a28';A['last_message_text']=E['message'];A['last_message_user_uid']=E[J];N=await B.app.services.user.get(uid=E[J]) - if D['tag']=='dm':A[K]=N[I] - A['last_message_user_color']=N[I];G.append(A) - G.sort(key=lambda x:x[C]or'',reverse=True);return await B.render_template('threads.html',{'threads':G,'user':M}) \ No newline at end of file + + async def get(self): + threads = [] + user = await self.services.user.get(uid=self.session.get("uid")) + async for channel_member in user.get_channel_members(): + thread = {} + channel = await self.services.channel.get(uid=channel_member["channel_uid"]) + last_message = await channel.get_last_message() + if not last_message: + continue + + thread["uid"] = channel["uid"] + thread["name"] = await channel_member.get_name() + thread["new_count"] = channel_member["new_count"] + thread["last_message_on"] = channel["last_message_on"] + thread["created_at"] = thread["last_message_on"] + + thread["name_color"] = "#f05a28" + thread["last_message_text"] = last_message["message"] + thread["last_message_user_uid"] = last_message["user_uid"] + user_last_message = await self.app.services.user.get( + uid=last_message["user_uid"] + ) + if channel["tag"] == "dm": + thread["name_color"] = user_last_message["color"] + thread["last_message_user_color"] = user_last_message["color"] + threads.append(thread) + + threads.sort(key=lambda x: x["last_message_on"] or "", reverse=True) + + return await self.render_template( + "threads.html", {"threads": threads, "user": user} + ) diff --git a/src/snek/view/upload.py b/src/snek/view/upload.py index e45d5f6..cf01948 100644 --- a/src/snek/view/upload.py +++ b/src/snek/view/upload.py @@ -1,19 +1,111 @@ -_A='uid' -import pathlib,uuid,aiofiles +# Written by retoor@molodetz.nl + +# This code defines a web application for uploading and retrieving files. +# It includes functionality to upload files through a POST request and retrieve them via a GET request. + +# The code uses the following non-standard imports: +# - snek.system.view.BaseView: For extending view functionalities. +# - aiofiles: For asynchronous file operations. +# - aiohttp: For managing web server requests and responses. + +# MIT License: This software is licensed under the MIT License, a permissive free software license. + +import pathlib +import uuid + +import aiofiles from aiohttp import web + from snek.system.view import BaseView + + class UploadView(BaseView): - async def get(B):D=B.request.match_info.get(_A);C=await B.services.drive_item.get(D);A=web.FileResponse(C['path']);A.headers['Cache-Control']=f"public, max-age={561540}";A.headers['Content-Disposition']=f'attachment; filename="{C["name"]}"';return A - async def post(A): - K='](/drive.bin/';L='channel_uid';G='document';D='image';P=await A.request.multipart();M=[];Q=A.request.session.get(_A);E=await A.services.user.get_home_folder(Q);E=E.joinpath('upload');E.mkdir(parents=True,exist_ok=True);H=None;R=await A.services.drive.get_or_create(user_uid=A.request.session.get(_A));N={'.jpg':D,'.gif':D,'.png':D,'.jpeg':D,'.mp4':'video','.mp3':'audio','.pdf':G,'.doc':G,'.docx':G} - while(F:=await P.next()): - if F.name==L:H=await F.text();continue - B=F.filename - if not B:continue - S=str(uuid.uuid4())+pathlib.Path(B).suffix;C=E.joinpath(S);M.append(C) - async with aiofiles.open(str(C),'wb')as T: - while(U:=await F.read_chunk()):await T.write(U) - I=await A.services.drive_item.create(R[_A],B,str(C),C.stat().st_size,C.suffix);J='.'+B.split('.')[-1] - if J in N:N[J] - await A.services.drive_item.save(I);O='Uploaded ['+B+K+I[_A]+')';O='['+B+K+I[_A]+J+')';await A.services.chat.send(A.request.session.get(_A),H,O) - return web.json_response({'message':'Files uploaded successfully','files':[str(A)for A in M],L:H}) \ No newline at end of file + + async def get(self): + uid = self.request.match_info.get("uid") + drive_item = await self.services.drive_item.get(uid) + response = web.FileResponse(drive_item["path"]) + response.headers["Cache-Control"] = f"public, max-age={1337*420}" + response.headers["Content-Disposition"] = ( + f'attachment; filename="{drive_item["name"]}"' + ) + return response + + async def post(self): + reader = await self.request.multipart() + files = [] + + user_uid = self.request.session.get("uid") + + upload_dir = await self.services.user.get_home_folder(user_uid) + upload_dir = upload_dir.joinpath("upload") + upload_dir.mkdir(parents=True, exist_ok=True) + + channel_uid = None + + drive = await self.services.drive.get_or_create( + user_uid=self.request.session.get("uid") + ) + + extension_types = { + ".jpg": "image", + ".gif": "image", + ".png": "image", + ".jpeg": "image", + ".mp4": "video", + ".mp3": "audio", + ".pdf": "document", + ".doc": "document", + ".docx": "document", + } + + while field := await reader.next(): + if field.name == "channel_uid": + channel_uid = await field.text() + continue + + filename = field.filename + if not filename: + continue + + name = str(uuid.uuid4()) + pathlib.Path(filename).suffix + + file_path = upload_dir.joinpath(name) + files.append(file_path) + + async with aiofiles.open(str(file_path), "wb") as f: + while chunk := await field.read_chunk(): + await f.write(chunk) + + drive_item = await self.services.drive_item.create( + drive["uid"], + filename, + str(file_path), + file_path.stat().st_size, + file_path.suffix, + ) + + extension = "." + filename.split(".")[-1] + if extension in extension_types: + extension_types[extension] + + await self.services.drive_item.save(drive_item) + response = ( + "Uploaded [" + filename + "](/drive.bin/" + drive_item["uid"] + ")" + ) + # response = "\n" + response = ( + "[" + filename + "](/drive.bin/" + drive_item["uid"] + extension + ")" + ) + + await self.services.chat.send( + self.request.session.get("uid"), channel_uid, response + ) + + return web.json_response( + { + "message": "Files uploaded successfully", + "files": [str(file) for file in files], + "channel_uid": channel_uid, + } + ) diff --git a/src/snek/view/user.py b/src/snek/view/user.py index ab00e1a..312f7bf 100644 --- a/src/snek/view/user.py +++ b/src/snek/view/user.py @@ -1,3 +1,15 @@ from snek.system.view import BaseView + + class UserView(BaseView): - async def get(A):B='profile';C='user';D=A.request.match_info.get(C);E=await A.services.user.get(uid=D);F=await A.services.user_property.get(E['uid'],B)or'';return await A.render_template('user.html',{'user_uid':D,C:E.record,B:F}) \ No newline at end of file + + async def get(self): + user_uid = self.request.match_info.get("user") + user = await self.services.user.get(uid=user_uid) + profile_content = ( + await self.services.user_property.get(user["uid"], "profile") or "" + ) + return await self.render_template( + "user.html", + {"user_uid": user_uid, "user": user.record, "profile": profile_content}, + ) diff --git a/src/snek/view/web.py b/src/snek/view/web.py index 292586d..111f76c 100644 --- a/src/snek/view/web.py +++ b/src/snek/view/web.py @@ -1,19 +1,79 @@ +# Written by retoor@molodetz.nl + +# This code defines a WebView class that inherits from BaseView and includes a method for rendering a web template, requiring login access for its usage. + +# The code imports the BaseView class from the `snek.system.view` module. + +# 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 aiohttp import web + from snek.system.view import BaseView + + class WebView(BaseView): - login_required=True - async def get(A): - F='channel';B='uid' - if A.login_required and not A.session.get('logged_in'):return web.HTTPFound('/') - C=await A.services.channel.get(uid=A.request.match_info.get(F)) - if not C: - D=await A.services.user.get(uid=A.request.match_info.get(F)) - if D: - C=await A.services.channel.get_dm(A.session.get(B),D[B]) - if C:return web.HTTPFound('/channel/{}.html'.format(C[B])) - if not C:return web.HTTPNotFound() - E=await A.app.services.channel_member.get(user_uid=A.session.get(B),channel_uid=C[B]) - if not E:return web.HTTPNotFound() - E['new_count']=0;await A.app.services.channel_member.save(E);D=await A.services.user.get(uid=A.session.get(B));G=[await A.app.services.channel_message.to_extended_dict(B)for B in await A.app.services.channel_message.offset(C[B])] - for H in G:await A.app.services.notification.mark_as_read(A.session.get(B),H[B]) - I=await E.get_name();return await A.render_template('web.html',{'name':I,F:C,'user':D,'messages':G}) \ No newline at end of file + login_required = True + + async def get(self): + if self.login_required and not self.session.get("logged_in"): + return web.HTTPFound("/") + channel = await self.services.channel.get( + uid=self.request.match_info.get("channel") + ) + if not channel: + user = await self.services.user.get( + uid=self.request.match_info.get("channel") + ) + if user: + channel = await self.services.channel.get_dm( + self.session.get("uid"), user["uid"] + ) + if channel: + return web.HTTPFound("/channel/{}.html".format(channel["uid"])) + if not channel: + return web.HTTPNotFound() + + channel_member = await self.app.services.channel_member.get( + user_uid=self.session.get("uid"), channel_uid=channel["uid"] + ) + if not channel_member: + return web.HTTPNotFound() + + channel_member["new_count"] = 0 + await self.app.services.channel_member.save(channel_member) + + user = await self.services.user.get(uid=self.session.get("uid")) + messages = [ + await self.app.services.channel_message.to_extended_dict(message) + for message in await self.app.services.channel_message.offset( + channel["uid"] + ) + ] + for message in messages: + await self.app.services.notification.mark_as_read( + self.session.get("uid"), message["uid"] + ) + + name = await channel_member.get_name() + return await self.render_template( + "web.html", + {"name": name, "channel": channel, "user": user, "messages": messages}, + ) diff --git a/src/snek/webdav.py b/src/snek/webdav.py index 0d0ae16..4c57fab 100755 --- a/src/snek/webdav.py +++ b/src/snek/webdav.py @@ -1,145 +1,377 @@ -_U='Lock-Token' -_T='application/xml' -_S='{DAV:}exclusive' -_R='{DAV:}lockdiscovery' -_Q='{DAV:}prop' -_P='%a, %d %b %Y %H:%M:%S GMT' -_O='Source not found' -_N='http://localhost:8080/' -_M='Destination' -_L='application/octet-stream' -_K='File not found' -_J='{DAV:}write' -_I='{DAV:}locktype' -_H='{DAV:}lockscope' -_G='{DAV:}href' -_F='Content-Type' -_E=True -_D='filename' -_C='Basic realm="WebDAV"' -_B='WWW-Authenticate' -_A='home' -import logging,pathlib +import logging +import pathlib + logging.basicConfig(level=logging.DEBUG) -import base64,datetime,mimetypes,os,shutil,uuid,aiofiles,aiohttp,aiohttp.web +import base64 +import datetime +import mimetypes +import os +import shutil +import uuid + +import aiofiles +import aiohttp +import aiohttp.web from app.cache import time_cache_async from lxml import etree + + @aiohttp.web.middleware -async def debug_middleware(request,handler): - A=request;print(A.method,A.path,A.headers);B=await handler(A);print(B.status) - try:print(await B.text()) - except:pass - return B +async def debug_middleware(request, handler): + print(request.method, request.path, request.headers) + result = await handler(request) + print(result.status) + try: + print(await result.text()) + except: + pass + return result + + class WebdavApplication(aiohttp.web.Application): - def __init__(A,parent,*C,**D):B='/{filename:.*}';E=[debug_middleware];super().__init__(*C,middlewares=E,**D);A.locks={};A.relative_url='/webdav';A.router.add_route('OPTIONS',B,A.handle_options);A.router.add_route('GET',B,A.handle_get);A.router.add_route('PUT',B,A.handle_put);A.router.add_route('DELETE',B,A.handle_delete);A.router.add_route('MKCOL',B,A.handle_mkcol);A.router.add_route('MOVE',B,A.handle_move);A.router.add_route('COPY',B,A.handle_copy);A.router.add_route('PROPFIND',B,A.handle_propfind);A.router.add_route('PROPPATCH',B,A.handle_proppatch);A.router.add_route('LOCK',B,A.handle_lock);A.router.add_route('UNLOCK',B,A.handle_unlock);A.parent=parent - @property - def db(self):return self.parent.db - @property - def services(self):return self.parent.services - async def authenticate(C,request): - D='Basic ';B='user';A=request;E=A.headers.get('Authorization','') - if not E.startswith(D):return False - F=E.split(D)[1];G=base64.b64decode(F).decode();H,I=G.split(':',1);A[B]=await C.services.user.authenticate(username=H,password=I) - try:A[_A]=await C.services.user.get_home_folder(A[B]['uid']) - except Exception:pass - return A[B] - async def handle_get(D,request): - B=request - if not await D.authenticate(B):return aiohttp.web.Response(status=401,headers={_B:_C}) - E=B.match_info.get(_D,'');A=B[_A]/E - if not A.exists():return aiohttp.web.Response(status=404,text=_K) - if A.is_dir():return aiohttp.web.Response(status=403,text='Cannot download a directory') - C,F=mimetypes.guess_type(str(A));C=C or _L;return aiohttp.web.FileResponse(path=str(A),headers={_F:C},chunk_size=8192) - async def handle_put(C,request): - A=request - if not await C.authenticate(A):return aiohttp.web.Response(status=401,headers={_B:_C}) - B=A[_A]/A.match_info[_D];B.parent.mkdir(parents=_E,exist_ok=_E) - async with aiofiles.open(B,'wb')as D: - while(E:=await A.content.read(1024)):await D.write(E) - return aiohttp.web.Response(status=201,text='File uploaded') - async def handle_delete(C,request): - B=request - if not await C.authenticate(B):return aiohttp.web.Response(status=401,headers={_B:_C}) - A=B[_A]/B.match_info[_D] - if A.is_file():A.unlink();return aiohttp.web.Response(status=204) - elif A.is_dir():shutil.rmtree(A);return aiohttp.web.Response(status=204) - return aiohttp.web.Response(status=404,text='Not found') - async def handle_mkcol(C,request): - A=request - if not await C.authenticate(A):return aiohttp.web.Response(status=401,headers={_B:_C}) - B=A[_A]/A.match_info[_D] - if B.exists():return aiohttp.web.Response(status=405,text='Directory already exists') - B.mkdir(parents=_E,exist_ok=_E);return aiohttp.web.Response(status=201,text='Directory created') - async def handle_move(C,request): - A=request - if not await C.authenticate(A):return aiohttp.web.Response(status=401,headers={_B:_C}) - B=A[_A]/A.match_info[_D];D=A[_A]/A.headers.get(_M,'').replace(_N,'') - if not B.exists():return aiohttp.web.Response(status=404,text=_O) - shutil.move(str(B),str(D));return aiohttp.web.Response(status=201,text='Moved successfully') - async def handle_copy(D,request): - A=request - if not await D.authenticate(A):return aiohttp.web.Response(status=401,headers={_B:_C}) - B=A[_A]/A.match_info[_D];C=A[_A]/A.headers.get(_M,'').replace(_N,'') - if not B.exists():return aiohttp.web.Response(status=404,text=_O) - if B.is_file():shutil.copy2(str(B),str(C)) - else:shutil.copytree(str(B),str(C)) - return aiohttp.web.Response(status=201,text='Copied successfully') - async def handle_options(B,request):A={'DAV':'1, 2','Allow':'OPTIONS, GET, PUT, DELETE, MKCOL, MOVE, COPY, PROPFIND, PROPPATCH'};return aiohttp.web.Response(status=200,headers=A) - def get_current_utc_time(C,filepath): - B=filepath - if B.exists():A=datetime.datetime.utcfromtimestamp(B.stat().st_mtime) - else:A=datetime.datetime.utcnow() - return A.strftime('%Y-%m-%dT%H:%M:%SZ'),A.strftime(_P) - @time_cache_async(10) - async def get_file_size(self,path):A=self.parent.loop;B=await A.run_in_executor(None,os.stat,path);return B.st_size - @time_cache_async(10) - async def get_directory_size(self,directory): - A=0 - for(C,F,D)in os.walk(directory): - for E in D: - B=pathlib.Path(C)/E - if B.exists():A+=await self.get_file_size(str(B)) - return A - @time_cache_async(30) - async def get_disk_free_space(self,path='/'):B=self.parent.loop;A=await B.run_in_executor(None,os.statvfs,path);return A.f_bavail*A.f_frsize - async def create_node(C,request,response_xml,full_path,depth): - F='{DAV:}lockentry';G=depth;H=response_xml;E=request;A=full_path;I=pathlib.Path(A);O=str(A.relative_to(E[_A]));D=f"{C.relative_url}/{O}".strip('.');D=D.replace('./','/');D=D.replace('//','/');J=etree.SubElement(H,'{DAV:}response');P=etree.SubElement(J,_G);P.text=D;K=etree.SubElement(J,'{DAV:}propstat');B=etree.SubElement(K,_Q);Q=etree.SubElement(B,'{DAV:}resourcetype') - if A.is_dir():etree.SubElement(Q,'{DAV:}collection') - R,S=C.get_current_utc_time(A);etree.SubElement(B,'{DAV:}creationdate').text=R;etree.SubElement(B,'{DAV:}quota-used-bytes').text=str(await C.get_file_size(A)if A.is_file()else await C.get_directory_size(A));etree.SubElement(B,'{DAV:}quota-available-bytes').text=str(await C.get_disk_free_space(E[_A]));etree.SubElement(B,'{DAV:}getlastmodified').text=S;etree.SubElement(B,'{DAV:}displayname').text=A.name;etree.SubElement(B,_R);T,Z=mimetypes.guess_type(A.name) - if A.is_file():etree.SubElement(B,'{DAV:}contenttype').text=T;etree.SubElement(B,'{DAV:}getcontentlength').text=str(await C.get_file_size(A)if A.is_file()else await C.get_directory_size(A)) - L=etree.SubElement(B,'{DAV:}supportedlock');M=etree.SubElement(L,F);U=etree.SubElement(M,_H);etree.SubElement(U,_S);V=etree.SubElement(M,_I);etree.SubElement(V,_J);N=etree.SubElement(L,F);W=etree.SubElement(N,_H);etree.SubElement(W,'{DAV:}shared');X=etree.SubElement(N,_I);etree.SubElement(X,_J);etree.SubElement(K,'{DAV:}status').text='HTTP/1.1 200 OK' - if I.is_dir()and G>0: - for Y in I.iterdir():await C.create_node(E,H,Y,G-1) - async def handle_propfind(B,request): - A=request - if not await B.authenticate(A):return aiohttp.web.Response(status=401,headers={_B:_C}) - C=0 - try:C=int(A.headers.get('Depth','0')) - except ValueError:pass - F=A.match_info.get(_D,'');D=A[_A]/F - if not D.exists():return aiohttp.web.Response(status=404,text='Directory not found') - G={'D':'DAV:'};E=etree.Element('{DAV:}multistatus',nsmap=G);await B.create_node(A,E,D,C);H=etree.tostring(E,encoding='utf-8',xml_declaration=_E).decode();return aiohttp.web.Response(status=207,text=H,content_type=_T) - async def handle_proppatch(A,request): - if not await A.authenticate(request):return aiohttp.web.Response(status=401,headers={_B:_C}) - return aiohttp.web.Response(status=207,text='PROPPATCH OK (Not Implemented)') - async def handle_lock(A,request): - C=request - if not await A.authenticate(C):return aiohttp.web.Response(status=401,headers={_B:_C}) - D=C.match_info.get(_D,'/');B=str(uuid.uuid4());A.locks[D]=B;E=await A.generate_lock_response(B);F={_U:f"opaquelocktoken:{B}",_F:_T};return aiohttp.web.Response(text=E,headers=F,status=200) - async def handle_unlock(A,request): - B=request - if not await A.authenticate(B):return aiohttp.web.Response(status=401,headers={_B:_C}) - C=B.match_info.get(_D,'/');D=B.headers.get(_U,'').replace('opaquelocktoken:','')[1:-1] - if A.locks.get(C)==D:del A.locks[C];return aiohttp.web.Response(status=204) - return aiohttp.web.Response(status=400,text='Invalid Lock Token') - async def generate_lock_response(J,lock_id):B=lock_id;D={'D':'DAV:'};C=etree.Element(_Q,nsmap=D);E=etree.SubElement(C,_R);A=etree.SubElement(E,'{DAV:}activelock');F=etree.SubElement(A,_I);etree.SubElement(F,_J);G=etree.SubElement(A,_H);etree.SubElement(G,_S);etree.SubElement(A,'{DAV:}depth').text='Infinity';H=etree.SubElement(A,'{DAV:}owner');etree.SubElement(H,_G).text=B;etree.SubElement(A,'{DAV:}timeout').text='Infinite';I=etree.SubElement(A,'{DAV:}locktoken');etree.SubElement(I,_G).text=f"opaquelocktoken:{B}";return etree.tostring(C,pretty_print=_E,encoding='utf-8').decode() - def get_last_modified(C,path): - if not path.exists():return - A=path.stat().st_mtime;B=datetime.datetime.utcfromtimestamp(A);return B.strftime(_P) - async def handle_head(D,request): - B=request - if not await D.authenticate(B):return aiohttp.web.Response(status=401,headers={_B:_C}) - E=B.match_info.get(_D,'');A=B[_A]/E - if not A.exists():return aiohttp.web.Response(status=404,text=_K) - if A.is_dir():return aiohttp.web.Response(status=403,text='Cannot get metadata for a directory') - C,H=mimetypes.guess_type(str(A));C=C or _L;F=A.stat().st_size;G={_F:C,'Content-Length':str(F),'Last-Modified':D.get_last_modified(A)};return aiohttp.web.Response(status=200,headers=G) \ No newline at end of file + def __init__(self, parent, *args, **kwargs): + middlewares = [debug_middleware] + + super().__init__(middlewares=middlewares, *args, **kwargs) + self.locks = {} + + self.relative_url = "/webdav" + + self.router.add_route("OPTIONS", "/{filename:.*}", self.handle_options) + self.router.add_route("GET", "/{filename:.*}", self.handle_get) + self.router.add_route("PUT", "/{filename:.*}", self.handle_put) + self.router.add_route("DELETE", "/{filename:.*}", self.handle_delete) + self.router.add_route("MKCOL", "/{filename:.*}", self.handle_mkcol) + self.router.add_route("MOVE", "/{filename:.*}", self.handle_move) + self.router.add_route("COPY", "/{filename:.*}", self.handle_copy) + self.router.add_route("PROPFIND", "/{filename:.*}", self.handle_propfind) + self.router.add_route("PROPPATCH", "/{filename:.*}", self.handle_proppatch) + self.router.add_route("LOCK", "/{filename:.*}", self.handle_lock) + self.router.add_route("UNLOCK", "/{filename:.*}", self.handle_unlock) + self.parent = parent + + @property + def db(self): + return self.parent.db + + @property + def services(self): + return self.parent.services + + async def authenticate(self, request): + auth_header = request.headers.get("Authorization", "") + if not auth_header.startswith("Basic "): + return False + encoded_creds = auth_header.split("Basic ")[1] + decoded_creds = base64.b64decode(encoded_creds).decode() + username, password = decoded_creds.split(":", 1) + request["user"] = await self.services.user.authenticate( + username=username, password=password + ) + try: + request["home"] = await self.services.user.get_home_folder( + request["user"]["uid"] + ) + except Exception: + pass + return request["user"] + + async def handle_get(self, request): + if not await self.authenticate(request): + return aiohttp.web.Response( + status=401, headers={"WWW-Authenticate": 'Basic realm="WebDAV"'} + ) + + requested_path = request.match_info.get("filename", "") + abs_path = request["home"] / requested_path + + if not abs_path.exists(): + return aiohttp.web.Response(status=404, text="File not found") + + if abs_path.is_dir(): + return aiohttp.web.Response(status=403, text="Cannot download a directory") + + content_type, _ = mimetypes.guess_type(str(abs_path)) + content_type = content_type or "application/octet-stream" + + return aiohttp.web.FileResponse( + path=str(abs_path), headers={"Content-Type": content_type}, chunk_size=8192 + ) + + async def handle_put(self, request): + if not await self.authenticate(request): + return aiohttp.web.Response( + status=401, headers={"WWW-Authenticate": 'Basic realm="WebDAV"'} + ) + file_path = request["home"] / request.match_info["filename"] + file_path.parent.mkdir(parents=True, exist_ok=True) + async with aiofiles.open(file_path, "wb") as f: + while chunk := await request.content.read(1024): + await f.write(chunk) + return aiohttp.web.Response(status=201, text="File uploaded") + + async def handle_delete(self, request): + if not await self.authenticate(request): + return aiohttp.web.Response( + status=401, headers={"WWW-Authenticate": 'Basic realm="WebDAV"'} + ) + file_path = request["home"] / request.match_info["filename"] + if file_path.is_file(): + file_path.unlink() + return aiohttp.web.Response(status=204) + elif file_path.is_dir(): + shutil.rmtree(file_path) + return aiohttp.web.Response(status=204) + return aiohttp.web.Response(status=404, text="Not found") + + async def handle_mkcol(self, request): + if not await self.authenticate(request): + return aiohttp.web.Response( + status=401, headers={"WWW-Authenticate": 'Basic realm="WebDAV"'} + ) + dir_path = request["home"] / request.match_info["filename"] + if dir_path.exists(): + return aiohttp.web.Response(status=405, text="Directory already exists") + dir_path.mkdir(parents=True, exist_ok=True) + return aiohttp.web.Response(status=201, text="Directory created") + + async def handle_move(self, request): + if not await self.authenticate(request): + return aiohttp.web.Response( + status=401, headers={"WWW-Authenticate": 'Basic realm="WebDAV"'} + ) + src_path = request["home"] / request.match_info["filename"] + dest_path = request["home"] / request.headers.get("Destination", "").replace( + "http://localhost:8080/", "" + ) + if not src_path.exists(): + return aiohttp.web.Response(status=404, text="Source not found") + shutil.move(str(src_path), str(dest_path)) + return aiohttp.web.Response(status=201, text="Moved successfully") + + async def handle_copy(self, request): + if not await self.authenticate(request): + return aiohttp.web.Response( + status=401, headers={"WWW-Authenticate": 'Basic realm="WebDAV"'} + ) + src_path = request["home"] / request.match_info["filename"] + dest_path = request["home"] / request.headers.get("Destination", "").replace( + "http://localhost:8080/", "" + ) + if not src_path.exists(): + return aiohttp.web.Response(status=404, text="Source not found") + if src_path.is_file(): + shutil.copy2(str(src_path), str(dest_path)) + else: + shutil.copytree(str(src_path), str(dest_path)) + return aiohttp.web.Response(status=201, text="Copied successfully") + + async def handle_options(self, request): + headers = { + "DAV": "1, 2", + "Allow": "OPTIONS, GET, PUT, DELETE, MKCOL, MOVE, COPY, PROPFIND, PROPPATCH", + } + return aiohttp.web.Response(status=200, headers=headers) + + def get_current_utc_time(self, filepath): + if filepath.exists(): + modified_time = datetime.datetime.utcfromtimestamp(filepath.stat().st_mtime) + else: + modified_time = datetime.datetime.utcnow() + return modified_time.strftime("%Y-%m-%dT%H:%M:%SZ"), modified_time.strftime( + "%a, %d %b %Y %H:%M:%S GMT" + ) + + @time_cache_async(10) + async def get_file_size(self, path): + loop = self.parent.loop + stat = await loop.run_in_executor(None, os.stat, path) + return stat.st_size + + @time_cache_async(10) + async def get_directory_size(self, directory): + total_size = 0 + for dirpath, _, filenames in os.walk(directory): + for f in filenames: + fp = pathlib.Path(dirpath) / f + if fp.exists(): + total_size += await self.get_file_size(str(fp)) + return total_size + + @time_cache_async(30) + async def get_disk_free_space(self, path="/"): + loop = self.parent.loop + statvfs = await loop.run_in_executor(None, os.statvfs, path) + return statvfs.f_bavail * statvfs.f_frsize + + async def create_node(self, request, response_xml, full_path, depth): + abs_path = pathlib.Path(full_path) + relative_path = str(full_path.relative_to(request["home"])) + + href_path = f"{self.relative_url}/{relative_path}".strip(".") + href_path = href_path.replace("./", "/") + href_path = href_path.replace("//", "/") + + response = etree.SubElement(response_xml, "{DAV:}response") + href = etree.SubElement(response, "{DAV:}href") + href.text = href_path + propstat = etree.SubElement(response, "{DAV:}propstat") + prop = etree.SubElement(propstat, "{DAV:}prop") + res_type = etree.SubElement(prop, "{DAV:}resourcetype") + if full_path.is_dir(): + etree.SubElement(res_type, "{DAV:}collection") + creation_date, last_modified = self.get_current_utc_time(full_path) + etree.SubElement(prop, "{DAV:}creationdate").text = creation_date + etree.SubElement(prop, "{DAV:}quota-used-bytes").text = str( + await self.get_file_size(full_path) + if full_path.is_file() + else await self.get_directory_size(full_path) + ) + etree.SubElement(prop, "{DAV:}quota-available-bytes").text = str( + await self.get_disk_free_space(request["home"]) + ) + etree.SubElement(prop, "{DAV:}getlastmodified").text = last_modified + etree.SubElement(prop, "{DAV:}displayname").text = full_path.name + etree.SubElement(prop, "{DAV:}lockdiscovery") + mimetype, _ = mimetypes.guess_type(full_path.name) + if full_path.is_file(): + etree.SubElement(prop, "{DAV:}contenttype").text = mimetype + etree.SubElement(prop, "{DAV:}getcontentlength").text = str( + await self.get_file_size(full_path) + if full_path.is_file() + else await self.get_directory_size(full_path) + ) + supported_lock = etree.SubElement(prop, "{DAV:}supportedlock") + lock_entry_1 = etree.SubElement(supported_lock, "{DAV:}lockentry") + lock_scope_1 = etree.SubElement(lock_entry_1, "{DAV:}lockscope") + etree.SubElement(lock_scope_1, "{DAV:}exclusive") + lock_type_1 = etree.SubElement(lock_entry_1, "{DAV:}locktype") + etree.SubElement(lock_type_1, "{DAV:}write") + lock_entry_2 = etree.SubElement(supported_lock, "{DAV:}lockentry") + lock_scope_2 = etree.SubElement(lock_entry_2, "{DAV:}lockscope") + etree.SubElement(lock_scope_2, "{DAV:}shared") + lock_type_2 = etree.SubElement(lock_entry_2, "{DAV:}locktype") + etree.SubElement(lock_type_2, "{DAV:}write") + etree.SubElement(propstat, "{DAV:}status").text = "HTTP/1.1 200 OK" + + if abs_path.is_dir() and depth > 0: + for item in abs_path.iterdir(): + await self.create_node(request, response_xml, item, depth - 1) + + async def handle_propfind(self, request): + if not await self.authenticate(request): + return aiohttp.web.Response( + status=401, headers={"WWW-Authenticate": 'Basic realm="WebDAV"'} + ) + + depth = 0 + try: + depth = int(request.headers.get("Depth", "0")) + except ValueError: + pass + + requested_path = request.match_info.get("filename", "") + + abs_path = request["home"] / requested_path + if not abs_path.exists(): + return aiohttp.web.Response(status=404, text="Directory not found") + nsmap = {"D": "DAV:"} + response_xml = etree.Element("{DAV:}multistatus", nsmap=nsmap) + + await self.create_node(request, response_xml, abs_path, depth) + + xml_output = etree.tostring( + response_xml, encoding="utf-8", xml_declaration=True + ).decode() + return aiohttp.web.Response( + status=207, text=xml_output, content_type="application/xml" + ) + + async def handle_proppatch(self, request): + if not await self.authenticate(request): + return aiohttp.web.Response( + status=401, headers={"WWW-Authenticate": 'Basic realm="WebDAV"'} + ) + return aiohttp.web.Response(status=207, text="PROPPATCH OK (Not Implemented)") + + async def handle_lock(self, request): + if not await self.authenticate(request): + return aiohttp.web.Response( + status=401, headers={"WWW-Authenticate": 'Basic realm="WebDAV"'} + ) + resource = request.match_info.get("filename", "/") + lock_id = str(uuid.uuid4()) + self.locks[resource] = lock_id + xml_response = await self.generate_lock_response(lock_id) + headers = { + "Lock-Token": f"opaquelocktoken:{lock_id}", + "Content-Type": "application/xml", + } + return aiohttp.web.Response(text=xml_response, headers=headers, status=200) + + async def handle_unlock(self, request): + if not await self.authenticate(request): + return aiohttp.web.Response( + status=401, headers={"WWW-Authenticate": 'Basic realm="WebDAV"'} + ) + resource = request.match_info.get("filename", "/") + lock_token = request.headers.get("Lock-Token", "").replace( + "opaquelocktoken:", "" + )[1:-1] + if self.locks.get(resource) == lock_token: + del self.locks[resource] + return aiohttp.web.Response(status=204) + return aiohttp.web.Response(status=400, text="Invalid Lock Token") + + async def generate_lock_response(self, lock_id): + nsmap = {"D": "DAV:"} + root = etree.Element("{DAV:}prop", nsmap=nsmap) + lock_discovery = etree.SubElement(root, "{DAV:}lockdiscovery") + active_lock = etree.SubElement(lock_discovery, "{DAV:}activelock") + lock_type = etree.SubElement(active_lock, "{DAV:}locktype") + etree.SubElement(lock_type, "{DAV:}write") + lock_scope = etree.SubElement(active_lock, "{DAV:}lockscope") + etree.SubElement(lock_scope, "{DAV:}exclusive") + etree.SubElement(active_lock, "{DAV:}depth").text = "Infinity" + owner = etree.SubElement(active_lock, "{DAV:}owner") + etree.SubElement(owner, "{DAV:}href").text = lock_id + etree.SubElement(active_lock, "{DAV:}timeout").text = "Infinite" + lock_token = etree.SubElement(active_lock, "{DAV:}locktoken") + etree.SubElement(lock_token, "{DAV:}href").text = f"opaquelocktoken:{lock_id}" + return etree.tostring(root, pretty_print=True, encoding="utf-8").decode() + + def get_last_modified(self, path): + if not path.exists(): + return None + timestamp = path.stat().st_mtime + dt = datetime.datetime.utcfromtimestamp(timestamp) + return dt.strftime("%a, %d %b %Y %H:%M:%S GMT") + + async def handle_head(self, request): + if not await self.authenticate(request): + return aiohttp.web.Response( + status=401, headers={"WWW-Authenticate": 'Basic realm="WebDAV"'} + ) + + requested_path = request.match_info.get("filename", "") + abs_path = request["home"] / requested_path + + if not abs_path.exists(): + return aiohttp.web.Response(status=404, text="File not found") + + if abs_path.is_dir(): + return aiohttp.web.Response( + status=403, text="Cannot get metadata for a directory" + ) + + content_type, _ = mimetypes.guess_type(str(abs_path)) + content_type = content_type or "application/octet-stream" + file_size = abs_path.stat().st_size + + headers = { + "Content-Type": content_type, + "Content-Length": str(file_size), + "Last-Modified": self.get_last_modified(abs_path), + } + + return aiohttp.web.Response(status=200, headers=headers) diff --git a/src/snekssh/app.py b/src/snekssh/app.py index a2087be..aab17e4 100644 --- a/src/snekssh/app.py +++ b/src/snekssh/app.py @@ -1,25 +1,78 @@ -_A=True -import asyncio,logging,os,asyncssh +import asyncio +import logging +import os + +import asyncssh + asyncssh.set_debug_level(2) logging.basicConfig(level=logging.DEBUG) -SFTP_ROOT='.' -USERNAME='test' -PASSWORD='woeii' -HOST='localhost' -PORT=2225 +# Configuration for SFTP server +SFTP_ROOT = "." # Directory to serve +USERNAME = "test" +PASSWORD = "woeii" +HOST = "localhost" +PORT = 2225 + + class MySFTPServer(asyncssh.SFTPServer): - def __init__(A,chan):super().__init__(chan);A.root=os.path.abspath(SFTP_ROOT) - async def stat(A,path):"Handles 'stat' command from SFTP client";B=os.path.join(A.root,path.lstrip('/'));return await super().stat(B) - async def open(A,path,flags,attrs):'Handles file open requests';B=os.path.join(A.root,path.lstrip('/'));return await super().open(B,flags,attrs) - async def listdir(A,path):'Handles directory listing';B=os.path.join(A.root,path.lstrip('/'));return await super().listdir(B) + def __init__(self, chan): + super().__init__(chan) + self.root = os.path.abspath(SFTP_ROOT) + + async def stat(self, path): + """Handles 'stat' command from SFTP client""" + full_path = os.path.join(self.root, path.lstrip("/")) + return await super().stat(full_path) + + async def open(self, path, flags, attrs): + """Handles file open requests""" + full_path = os.path.join(self.root, path.lstrip("/")) + return await super().open(full_path, flags, attrs) + + async def listdir(self, path): + """Handles directory listing""" + full_path = os.path.join(self.root, path.lstrip("/")) + return await super().listdir(full_path) + + class MySSHServer(asyncssh.SSHServer): - 'Custom SSH server to handle authentication' - def connection_made(A,conn):print(f"New connection from {conn.get_extra_info("peername")}") - def connection_lost(A,exc):print('Client disconnected') - def begin_auth(A,username):return _A - def password_auth_supported(A):return _A - def validate_password(C,username,password):A=password;B=username;print(B,A);return _A;return B==USERNAME and A==PASSWORD -async def start_sftp_server():os.makedirs(SFTP_ROOT,exist_ok=_A);await asyncssh.create_server(lambda:MySSHServer(),host=HOST,port=PORT,server_host_keys=['ssh_host_key'],process_factory=MySFTPServer);print(f"SFTP server running on {HOST}:{PORT}");await asyncio.Future() -if __name__=='__main__': - try:asyncio.run(start_sftp_server()) - except(OSError,asyncssh.Error)as e:print(f"Error starting SFTP server: {e}") \ No newline at end of file + """Custom SSH server to handle authentication""" + + def connection_made(self, conn): + print(f"New connection from {conn.get_extra_info('peername')}") + + def connection_lost(self, exc): + print("Client disconnected") + + def begin_auth(self, username): + return True # No additional authentication steps + + def password_auth_supported(self): + return True # Support password authentication + + def validate_password(self, username, password): + print(username, password) + + return True + return username == USERNAME and password == PASSWORD + + +async def start_sftp_server(): + os.makedirs(SFTP_ROOT, exist_ok=True) # Ensure the root directory exists + + await asyncssh.create_server( + lambda: MySSHServer(), + host=HOST, + port=PORT, + server_host_keys=["ssh_host_key"], + process_factory=MySFTPServer, + ) + print(f"SFTP server running on {HOST}:{PORT}") + await asyncio.Future() # Keep running forever + + +if __name__ == "__main__": + try: + asyncio.run(start_sftp_server()) + except (OSError, asyncssh.Error) as e: + print(f"Error starting SFTP server: {e}") diff --git a/src/snekssh/app2.py b/src/snekssh/app2.py index 8879a05..2fa26a7 100644 --- a/src/snekssh/app2.py +++ b/src/snekssh/app2.py @@ -1,28 +1,77 @@ -import asyncio,os,asyncssh -HOST='0.0.0.0' -PORT=2225 -USERNAME='user' -PASSWORD='password' -SHELL='/bin/sh' +import asyncio +import os + +import asyncssh + +# SSH Server Configuration +HOST = "0.0.0.0" +PORT = 2225 +USERNAME = "user" +PASSWORD = "password" +SHELL = "/bin/sh" # Change to another shell if needed + + class CustomSSHServer(asyncssh.SSHServer): - def connection_made(A,conn):print(f"New connection from {conn.get_extra_info("peername")}") - def connection_lost(A,exc):print('Client disconnected') - def password_auth_supported(A):return True - def validate_password(A,username,password):return username==USERNAME and password==PASSWORD + def connection_made(self, conn): + print(f"New connection from {conn.get_extra_info('peername')}") + + def connection_lost(self, exc): + print("Client disconnected") + + def password_auth_supported(self): + return True + + def validate_password(self, username, password): + return username == USERNAME and password == PASSWORD + + async def custom_bash_process(process): - 'Spawns a custom bash shell process';A=process;B=os.environ.copy();B['TERM']='xterm-256color';C=await asyncio.create_subprocess_exec(SHELL,'-i',stdin=asyncio.subprocess.PIPE,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.PIPE,env=B) - async def D(): - while True: - B=await C.stdout.read(1) - if not B:break - A.stdout.write(B) - async def E(): - while True: - B=await A.stdin.read(1) - if not B:break - C.stdin.write(B) - await asyncio.gather(D(),E()) -async def start_ssh_server():'Starts the AsyncSSH server with Bash';await asyncssh.create_server(lambda:CustomSSHServer(),host=HOST,port=PORT,server_host_keys=['ssh_host_key'],process_factory=custom_bash_process);print(f"SSH server running on {HOST}:{PORT}");await asyncio.Future() -if __name__=='__main__': - try:asyncio.run(start_ssh_server()) - except(OSError,asyncssh.Error)as e:print(f"Error starting SSH server: {e}") \ No newline at end of file + """Spawns a custom bash shell process""" + env = os.environ.copy() + env["TERM"] = "xterm-256color" + + # Start the Bash shell + bash_proc = await asyncio.create_subprocess_exec( + SHELL, + "-i", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=env, + ) + + async def read_output(): + while True: + data = await bash_proc.stdout.read(1) + if not data: + break + process.stdout.write(data) + + async def read_input(): + while True: + data = await process.stdin.read(1) + if not data: + break + bash_proc.stdin.write(data) + + await asyncio.gather(read_output(), read_input()) + + +async def start_ssh_server(): + """Starts the AsyncSSH server with Bash""" + await asyncssh.create_server( + lambda: CustomSSHServer(), + host=HOST, + port=PORT, + server_host_keys=["ssh_host_key"], + process_factory=custom_bash_process, + ) + print(f"SSH server running on {HOST}:{PORT}") + await asyncio.Future() # Keep running + + +if __name__ == "__main__": + try: + asyncio.run(start_ssh_server()) + except (OSError, asyncssh.Error) as e: + print(f"Error starting SSH server: {e}") diff --git a/src/snekssh/app3.py b/src/snekssh/app3.py index ef35691..4a09452 100644 --- a/src/snekssh/app3.py +++ b/src/snekssh/app3.py @@ -1,17 +1,74 @@ #!/usr/bin/env python3.7 -import asyncio,sys,asyncssh -async def handle_client(process): - A=process;E,F,C,D=A.term_size;A.stdout.write(f"Terminal type: {A.term_type}, size: {E}x{F}") - if C and D:A.stdout.write(f" ({C}x{D} pixels)") - A.stdout.write('\nTry resizing your window!\n') - while not A.stdin.at_eof(): - try:await A.stdin.read() - except asyncssh.TerminalSizeChanged as B: - A.stdout.write(f"New window size: {B.width}x{B.height}") - if B.pixwidth and B.pixheight:A.stdout.write(f" ({B.pixwidth}x{B.pixheight} pixels)") - A.stdout.write('\n') -async def start_server():await asyncssh.listen('',2230,server_host_keys=['ssh_host_key'],process_factory=handle_client) -loop=asyncio.new_event_loop() -try:loop.run_until_complete(start_server()) -except(OSError,asyncssh.Error)as exc:sys.exit('Error starting server: '+str(exc)) -loop.run_forever() \ No newline at end of file +# +# Copyright (c) 2013-2024 by Ron Frederick and others. +# +# This program and the accompanying materials are made available under +# the terms of the Eclipse Public License v2.0 which accompanies this +# distribution and is available at: +# +# http://www.eclipse.org/legal/epl-2.0/ +# +# This program may also be made available under the following secondary +# licenses when the conditions for such availability set forth in the +# Eclipse Public License v2.0 are satisfied: +# +# GNU General Public License, Version 2.0, or any later versions of +# that license +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later +# +# Contributors: +# Ron Frederick - initial implementation, API, and documentation + +# To run this program, the file ``ssh_host_key`` must exist with an SSH +# private key in it to use as a server host key. An SSH host certificate +# can optionally be provided in the file ``ssh_host_key-cert.pub``. +# +# The file ``ssh_user_ca`` must exist with a cert-authority entry of +# the certificate authority which can sign valid client certificates. + +import asyncio +import sys + +import asyncssh + + +async def handle_client(process: asyncssh.SSHServerProcess) -> None: + + width, height, pixwidth, pixheight = process.term_size + + process.stdout.write( + f"Terminal type: {process.term_type}, " f"size: {width}x{height}" + ) + if pixwidth and pixheight: + process.stdout.write(f" ({pixwidth}x{pixheight} pixels)") + process.stdout.write("\nTry resizing your window!\n") + + while not process.stdin.at_eof(): + try: + await process.stdin.read() + except asyncssh.TerminalSizeChanged as exc: + process.stdout.write(f"New window size: {exc.width}x{exc.height}") + if exc.pixwidth and exc.pixheight: + process.stdout.write(f" ({exc.pixwidth}" f"x{exc.pixheight} pixels)") + process.stdout.write("\n") + + +async def start_server() -> None: + await asyncssh.listen( + "", + 2230, + server_host_keys=["ssh_host_key"], + # authorized_client_keys='ssh_user_ca', + process_factory=handle_client, + ) + + +loop = asyncio.new_event_loop() + +try: + loop.run_until_complete(start_server()) +except (OSError, asyncssh.Error) as exc: + sys.exit("Error starting server: " + str(exc)) + +loop.run_forever() diff --git a/src/snekssh/app4.py b/src/snekssh/app4.py index eb4fe72..187722c 100644 --- a/src/snekssh/app4.py +++ b/src/snekssh/app4.py @@ -1,24 +1,90 @@ #!/usr/bin/env python3.7 -import asyncio,sys +# +# Copyright (c) 2013-2024 by Ron Frederick and others. +# +# This program and the accompanying materials are made available under +# the terms of the Eclipse Public License v2.0 which accompanies this +# distribution and is available at: +# +# http://www.eclipse.org/legal/epl-2.0/ +# +# This program may also be made available under the following secondary +# licenses when the conditions for such availability set forth in the +# Eclipse Public License v2.0 are satisfied: +# +# GNU General Public License, Version 2.0, or any later versions of +# that license +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later +# +# Contributors: +# Ron Frederick - initial implementation, API, and documentation + +# To run this program, the file ``ssh_host_key`` must exist with an SSH +# private key in it to use as a server host key. An SSH host certificate +# can optionally be provided in the file ``ssh_host_key-cert.pub``. + +import asyncio +import sys from typing import Optional -import asyncssh,bcrypt -passwords={'guest':b'','user':bcrypt.hashpw(b'user',bcrypt.gensalt())} -def handle_client(process):A=process;B=A.get_extra_info('username');A.stdout.write(f"Welcome to my SSH server, {B}!\n") + +import asyncssh +import bcrypt + +passwords = { + "guest": b"", # guest account with no password + "user": bcrypt.hashpw(b"user", bcrypt.gensalt()), +} + + +def handle_client(process: asyncssh.SSHServerProcess) -> None: + username = process.get_extra_info("username") + process.stdout.write(f"Welcome to my SSH server, {username}!\n") + # process.exit(0) + + class MySSHServer(asyncssh.SSHServer): - def connection_made(B,conn):A=conn.get_extra_info('peername')[0];print(f"SSH connection received from {A}.") - def connection_lost(A,exc): - if exc:print('SSH connection error: '+str(exc),file=sys.stderr) - else:print('SSH connection closed.') - def begin_auth(A,username):return passwords.get(username)!=b'' - def password_auth_supported(A):return True - def validate_password(D,username,password): - A=password;B=username - if B not in passwords:return False - C=passwords[B] - if not A and not C:return True - return bcrypt.checkpw(A.encode('utf-8'),C) -async def start_server():await asyncssh.create_server(MySSHServer,'',2231,server_host_keys=['ssh_host_key'],process_factory=handle_client) -loop=asyncio.new_event_loop() -try:loop.run_until_complete(start_server()) -except(OSError,asyncssh.Error)as exc:sys.exit('Error starting server: '+str(exc)) -loop.run_forever() \ No newline at end of file + def connection_made(self, conn: asyncssh.SSHServerConnection) -> None: + peername = conn.get_extra_info("peername")[0] + print(f"SSH connection received from {peername}.") + + def connection_lost(self, exc: Optional[Exception]) -> None: + if exc: + print("SSH connection error: " + str(exc), file=sys.stderr) + else: + print("SSH connection closed.") + + def begin_auth(self, username: str) -> bool: + # If the user's password is the empty string, no auth is required + return passwords.get(username) != b"" + + def password_auth_supported(self) -> bool: + return True + + def validate_password(self, username: str, password: str) -> bool: + if username not in passwords: + return False + pw = passwords[username] + if not password and not pw: + return True + return bcrypt.checkpw(password.encode("utf-8"), pw) + + +async def start_server() -> None: + await asyncssh.create_server( + MySSHServer, + "", + 2231, + server_host_keys=["ssh_host_key"], + process_factory=handle_client, + ) + + +loop = asyncio.new_event_loop() + +try: + loop.run_until_complete(start_server()) +except (OSError, asyncssh.Error) as exc: + sys.exit("Error starting server: " + str(exc)) + +loop.run_forever() diff --git a/src/snekssh/app5.py b/src/snekssh/app5.py index 39f45e3..cfd5d21 100644 --- a/src/snekssh/app5.py +++ b/src/snekssh/app5.py @@ -1,28 +1,112 @@ #!/usr/bin/env python3.7 -import asyncio,sys -from typing import List,cast +# +# Copyright (c) 2016-2024 by Ron Frederick and others. +# +# This program and the accompanying materials are made available under +# the terms of the Eclipse Public License v2.0 which accompanies this +# distribution and is available at: +# +# http://www.eclipse.org/legal/epl-2.0/ +# +# This program may also be made available under the following secondary +# licenses when the conditions for such availability set forth in the +# Eclipse Public License v2.0 are satisfied: +# +# GNU General Public License, Version 2.0, or any later versions of +# that license +# +# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later +# +# Contributors: +# Ron Frederick - initial implementation, API, and documentation + +# To run this program, the file ``ssh_host_key`` must exist with an SSH +# private key in it to use as a server host key. An SSH host certificate +# can optionally be provided in the file ``ssh_host_key-cert.pub``. +# +# The file ``ssh_user_ca`` must exist with a cert-authority entry of +# the certificate authority which can sign valid client certificates. + +import asyncio +import sys +from typing import List, cast + import asyncssh + + class ChatClient: - _clients:List['ChatClient']=[] - def __init__(A,process):A._process=process - @classmethod - async def handle_client(A,process):await A(process).run() - async def readline(A):return cast(str,A._process.stdin.readline()) - def write(A,msg):A._process.stdout.write(msg) - def broadcast(A,msg): - for B in A._clients: - if B!=A:B.write(msg) - def begin_auth(A,username):return True - def password_auth_supported(A):return True - def validate_password(A,username,password):return True - async def run(A): - A.write('Welcome to chat!\n\n');A.write('Enter your name: ');B=(await A.readline()).rstrip('\n');A.write(f"\n{len(A._clients)} other users are connected.\n\n");A._clients.append(A);A.broadcast(f"*** {B} has entered chat ***\n") - try: - async for C in A._process.stdin:A.broadcast(f"{B}: {C}") - except asyncssh.BreakReceived:pass - A.broadcast(f"*** {B} has left chat ***\n");A._clients.remove(A) -async def start_server():await asyncssh.listen('',2235,server_host_keys=['ssh_host_key'],process_factory=ChatClient.handle_client) -loop=asyncio.new_event_loop() -try:loop.run_until_complete(start_server()) -except(OSError,asyncssh.Error)as exc:sys.exit('Error starting server: '+str(exc)) -loop.run_forever() \ No newline at end of file + _clients: List["ChatClient"] = [] + + def __init__(self, process: asyncssh.SSHServerProcess): + self._process = process + + @classmethod + async def handle_client(cls, process: asyncssh.SSHServerProcess): + await cls(process).run() + + async def readline(self) -> str: + return cast(str, self._process.stdin.readline()) + + def write(self, msg: str) -> None: + self._process.stdout.write(msg) + + def broadcast(self, msg: str) -> None: + for client in self._clients: + if client != self: + client.write(msg) + + def begin_auth(self, username: str) -> bool: + # If the user's password is the empty string, no auth is required + # return False + return True # passwords.get(username) != b'' + + def password_auth_supported(self) -> bool: + return True + + def validate_password(self, username: str, password: str) -> bool: + # if username not in passwords: + # return False + # pw = passwords[username] + # if not password and not pw: + # return True + return True + # return bcrypt.checkpw(password.encode('utf-8'), pw) + + async def run(self) -> None: + self.write("Welcome to chat!\n\n") + + self.write("Enter your name: ") + name = (await self.readline()).rstrip("\n") + + self.write(f"\n{len(self._clients)} other users are connected.\n\n") + + self._clients.append(self) + self.broadcast(f"*** {name} has entered chat ***\n") + + try: + async for line in self._process.stdin: + self.broadcast(f"{name}: {line}") + except asyncssh.BreakReceived: + pass + + self.broadcast(f"*** {name} has left chat ***\n") + self._clients.remove(self) + + +async def start_server() -> None: + await asyncssh.listen( + "", + 2235, + server_host_keys=["ssh_host_key"], + process_factory=ChatClient.handle_client, + ) + + +loop = asyncio.new_event_loop() + +try: + loop.run_until_complete(start_server()) +except (OSError, asyncssh.Error) as exc: + sys.exit("Error starting server: " + str(exc)) + +loop.run_forever()