Compare commits
137
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bde3819510 | ||
|
|
097889ba3f | ||
|
|
4854d40508 | ||
|
|
7dd3133475 | ||
|
|
24dfa39f91 | ||
|
|
7ec65f7c12 | ||
|
|
4f8edef42b | ||
|
|
7818410d55 | ||
|
|
1762191b03 | ||
|
|
2df92e809e | ||
|
|
59a8d32e40 | ||
|
|
c3b3963760 | ||
|
|
a0cd39e3bc | ||
|
|
e48b2258e0 | ||
|
|
35aaf8824f | ||
|
|
76c69ca3ec | ||
|
|
9994225911 | ||
|
|
03f699e448 | ||
|
|
2fd01a5ab7 | ||
|
|
96629113f1 | ||
|
|
973afa0cc2 | ||
|
|
9bc55e771a | ||
|
|
40a292d05e | ||
|
|
bf723db2cc | ||
|
|
46052172b2 | ||
|
|
4f777f0003 | ||
|
|
b5e1ba72d0 | ||
|
|
df120098f9 | ||
|
|
1c1d578db7 | ||
|
|
69352fe0b5 | ||
|
|
fb3980dad0 | ||
|
|
6c21a1e619 | ||
|
|
112c0dc70a | ||
|
|
538a9ce25d | ||
|
|
cdc3d10df5 | ||
|
|
1b150e3e64 | ||
|
|
27dccc324a | ||
|
|
1bb68ab33b | ||
|
|
3c6ea15d47 | ||
|
|
36e663e1ed | ||
|
|
8d2e0381a7 | ||
|
|
f67d7b35f1 | ||
|
|
9ec62f7471 | ||
|
|
6bbbc41360 | ||
|
|
d3844ac7a7 | ||
|
|
9378e95a5b | ||
|
|
5e4c4ce228 | ||
|
|
ffc373db62 | ||
|
|
60266bf0dc | ||
|
|
8393a80022 | ||
|
|
234edf4756 | ||
|
|
5fd401bfb6 | ||
|
|
5663a5f376 | ||
|
|
81327a9e20 | ||
|
|
2a5b9ad276 | ||
|
|
662e71c621 | ||
|
|
636adfd997 | ||
|
|
b94f7a9532 | ||
|
|
f954a34384 | ||
|
|
6a74263606 | ||
|
|
0bf714061c | ||
|
|
e4e2e919c2 | ||
|
|
7fe4289f42 | ||
|
|
3ce866b7da | ||
|
|
43982c16fa | ||
|
|
e33e4196ab | ||
|
|
2c506db4e4 | ||
|
|
9b9d356849 | ||
|
|
30b7871583 | ||
|
|
1c873b7d02 | ||
|
|
539fb262b2 | ||
|
|
3bf09f9083 | ||
|
|
a55d15b635 | ||
|
|
431748c489 | ||
|
|
a0fb214332 | ||
|
|
f0545cbf02 | ||
|
|
a11c336cf5 | ||
|
|
6b083f8b1b | ||
|
|
89afbba165 | ||
|
|
b2a4887e23 | ||
|
|
9f577875f2 | ||
|
|
c322d6147a | ||
|
|
87b6b3362d | ||
|
|
d261f54327 | ||
|
|
59a815f85a | ||
|
|
2e837f96c5 | ||
|
|
00fce6bd68 | ||
|
|
8a85cd7990 | ||
|
|
db5431d77d | ||
|
|
527b010b24 | ||
|
|
e1727caa5f | ||
|
|
c45b61681d | ||
|
|
e09652413f | ||
|
|
0f337e569f | ||
|
|
59a2668c8c | ||
|
|
e79abf4a26 | ||
|
|
53811ca9b2 | ||
|
|
1bed47fbf5 | ||
|
|
ffb22165da | ||
|
|
48c3daf398 | ||
|
|
c0b4ba715c | ||
|
|
00557ec9ea | ||
|
|
c387225a6e | ||
|
|
93462d4c4b | ||
|
|
c5b55399a1 | ||
|
|
79c39828f0 | ||
|
|
dd80f3732b | ||
|
|
25d109beed | ||
|
|
db6d6c0106 | ||
|
|
af1cf4f5ae | ||
|
|
0ea0cd96db | ||
|
|
3858dcbd62 | ||
|
|
b55d74fb12 | ||
|
|
a21e3590ef | ||
|
|
319c1b1b52 | ||
|
|
964a747f42 | ||
|
|
12d2870424 | ||
|
|
015b188d5e | ||
|
|
8cd2f16c5c | ||
|
|
d09055986e | ||
|
|
2e324ff118 | ||
|
|
adad5ed4fe | ||
|
|
ba3152f553 | ||
|
|
a4bea94495 | ||
|
|
ac2f68f93f | ||
|
|
f156a153de | ||
|
|
c48b84bf3a | ||
|
|
01846bf23f | ||
|
|
2c90044185 | ||
|
|
4d7566de9b | ||
|
|
9133b7c3ce | ||
|
|
3412aa0bf0 | ||
|
|
f0591d4939 | ||
|
|
dd108c2004 | ||
|
|
44ac1d2bfa | ||
|
|
1616e4edb9 | ||
|
|
4c34d7eda5 |
@@ -20,7 +20,6 @@ serve: run
|
|||||||
|
|
||||||
run:
|
run:
|
||||||
.venv/bin/snek serve
|
.venv/bin/snek serve
|
||||||
#$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload
|
|
||||||
|
|
||||||
install: ubuntu
|
install: ubuntu
|
||||||
python3.12 -m venv .venv
|
python3.12 -m venv .venv
|
||||||
|
|||||||
+6
-2
@@ -18,7 +18,8 @@ dependencies = [
|
|||||||
"lxml",
|
"lxml",
|
||||||
"IPython",
|
"IPython",
|
||||||
"shed",
|
"shed",
|
||||||
"app @ git+https://retoor.molodetz.nl/retoor/app",
|
"app @ git+https://retoor.molodetz.nl/retoor/app.git",
|
||||||
|
"zhurnal @git+https://retoor.molodetz.nl/retoor/zhurnal.git",
|
||||||
"beautifulsoup4",
|
"beautifulsoup4",
|
||||||
"gunicorn",
|
"gunicorn",
|
||||||
"imgkit",
|
"imgkit",
|
||||||
@@ -33,7 +34,10 @@ dependencies = [
|
|||||||
"PyJWT",
|
"PyJWT",
|
||||||
"multiavatar",
|
"multiavatar",
|
||||||
"gitpython",
|
"gitpython",
|
||||||
"uvloop"
|
'uvloop; platform_system != "Windows"',
|
||||||
|
"humanize",
|
||||||
|
"Pillow",
|
||||||
|
"pillow-heif",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
+50
-14
@@ -1,21 +1,57 @@
|
|||||||
_D='Database path for the application'
|
import click
|
||||||
_C='snek.db'
|
import uvloop
|
||||||
_B='--db_path'
|
|
||||||
_A=True
|
|
||||||
import click,uvloop
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import asyncio
|
import asyncio
|
||||||
from snek.app import Application
|
from snek.app import Application
|
||||||
from IPython import start_ipython
|
from IPython import start_ipython
|
||||||
|
import sqlite3
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def cli():0
|
def cli():
|
||||||
|
pass
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option('--port',default=8081,show_default=_A,help='Port to run the application on')
|
@click.option('--db_path',default="snek.db", help='Database to initialize if not exists.')
|
||||||
@click.option('--host',default='0.0.0.0',show_default=_A,help='Host to run the application on')
|
@click.option('--source',default=None, help='Database to initialize if not exists.')
|
||||||
@click.option(_B,default=_C,show_default=_A,help=_D)
|
def init(db_path,source):
|
||||||
def serve(port,host,db_path):asyncio.set_event_loop_policy(uvloop.EventLoopPolicy());web.run_app(Application(db_path=f"sqlite:///{db_path}"),port=port,host=host)
|
if source and pathlib.Path(source).exists():
|
||||||
|
print(f"Copying {source} to {db_path}")
|
||||||
|
shutil.copy2(source,db_path)
|
||||||
|
print("Database initialized.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if pathlib.Path(db_path).exists():
|
||||||
|
return
|
||||||
|
print(f"Initializing database at {db_path}")
|
||||||
|
db = sqlite3.connect(db_path)
|
||||||
|
db.cursor().executescript(
|
||||||
|
pathlib.Path(__file__).parent.joinpath("schema.sql").read_text()
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
db.close()
|
||||||
|
print("Database initialized.")
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option(_B,default=_C,show_default=_A,help=_D)
|
@click.option('--port', default=8081, show_default=True, help='Port to run the application on')
|
||||||
def shell(db_path):A=Application(db_path=f"sqlite:///{db_path}");start_ipython(argv=[],user_ns={'app':A})
|
@click.option('--host', default='0.0.0.0', show_default=True, help='Host to run the application on')
|
||||||
def main():cli()
|
@click.option('--db_path', default='snek.db', show_default=True, help='Database path for the application')
|
||||||
if __name__=='__main__':main()
|
def serve(port, host, db_path):
|
||||||
|
#init(db_path)
|
||||||
|
#asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
web.run_app(
|
||||||
|
Application(db_path=f"sqlite:///{db_path}"), port=port, host=host
|
||||||
|
)
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option('--db_path', default='snek.db', show_default=True, help='Database path for the application')
|
||||||
|
def shell(db_path):
|
||||||
|
app = Application(db_path=f"sqlite:///{db_path}")
|
||||||
|
start_ipython(argv=[], user_ns={'app': app})
|
||||||
|
|
||||||
|
def main():
|
||||||
|
cli()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
+334
-73
@@ -1,19 +1,27 @@
|
|||||||
_G='name'
|
import asyncio
|
||||||
_F='static'
|
import logging
|
||||||
_E='user'
|
import pathlib
|
||||||
_D=None
|
import time
|
||||||
_C=True
|
import uuid
|
||||||
_B='channel_uid'
|
from datetime import datetime
|
||||||
_A='uid'
|
from snek import snode
|
||||||
import asyncio,logging,pathlib,time,uuid
|
|
||||||
from snek.view.threads import ThreadsView
|
from snek.view.threads import ThreadsView
|
||||||
|
import json
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp_session import get_session as session_get,session_middleware,setup as session_setup
|
from aiohttp_session import (
|
||||||
|
get_session as session_get,
|
||||||
|
session_middleware,
|
||||||
|
setup as session_setup,
|
||||||
|
)
|
||||||
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
||||||
from app.app import Application as BaseApplication
|
from app.app import Application as BaseApplication
|
||||||
from jinja2 import FileSystemLoader
|
from jinja2 import FileSystemLoader
|
||||||
|
|
||||||
|
from snek.sssh import start_ssh_server
|
||||||
from snek.docs.app import Application as DocsApplication
|
from snek.docs.app import Application as DocsApplication
|
||||||
from snek.mapper import get_mappers
|
from snek.mapper import get_mappers
|
||||||
from snek.service import get_services
|
from snek.service import get_services
|
||||||
@@ -27,6 +35,7 @@ from snek.view.about import AboutHTMLView,AboutMDView
|
|||||||
from snek.view.avatar import AvatarView
|
from snek.view.avatar import AvatarView
|
||||||
from snek.view.docs import DocsHTMLView, DocsMDView
|
from snek.view.docs import DocsHTMLView, DocsMDView
|
||||||
from snek.view.drive import DriveView
|
from snek.view.drive import DriveView
|
||||||
|
from snek.view.drive import DriveApiView
|
||||||
from snek.view.index import IndexView
|
from snek.view.index import IndexView
|
||||||
from snek.view.login import LoginView
|
from snek.view.login import LoginView
|
||||||
from snek.view.logout import LogoutView
|
from snek.view.logout import LogoutView
|
||||||
@@ -46,75 +55,327 @@ from snek.view.terminal import TerminalSocketView,TerminalView
|
|||||||
from snek.view.upload import UploadView
|
from snek.view.upload import UploadView
|
||||||
from snek.view.user import UserView
|
from snek.view.user import UserView
|
||||||
from snek.view.web import WebView
|
from snek.view.web import WebView
|
||||||
|
from snek.view.channel import ChannelAttachmentView
|
||||||
|
from snek.view.channel import ChannelView
|
||||||
|
from snek.view.settings.containers import ContainersIndexView, ContainersCreateView, ContainersUpdateView, ContainersDeleteView
|
||||||
from snek.webdav import WebdavApplication
|
from snek.webdav import WebdavApplication
|
||||||
from snek.sgit import GitApplication
|
from snek.sgit import GitApplication
|
||||||
SESSION_KEY=b'c79a0c5fda4b424189c427d28c9f7c34'
|
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def session_middleware(request,handler):A=request;setattr(A,'session',await session_get(A));B=await handler(A);return B
|
async def session_middleware(request, handler):
|
||||||
|
setattr(request, "session", await session_get(request))
|
||||||
|
response = await handler(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def trailing_slash_middleware(request, handler):
|
async def trailing_slash_middleware(request, handler):
|
||||||
A=request
|
if request.path and not request.path.endswith("/"):
|
||||||
if A.path and not A.path.endswith('/'):raise web.HTTPFound(A.path+'/')
|
# Redirect to the same path with a trailing slash
|
||||||
return await handler(A)
|
raise web.HTTPFound(request.path + "/")
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
|
||||||
class Application(BaseApplication):
|
class Application(BaseApplication):
|
||||||
def __init__(A,*B,**C):D=[cors_middleware,web.normalize_path_middleware(merge_slashes=_C)];A.template_path=pathlib.Path(__file__).parent.joinpath('templates');A.static_path=pathlib.Path(__file__).parent.joinpath(_F);super().__init__(middlewares=D,template_path=A.template_path,client_max_size=5368709120*B,**C);session_setup(A,EncryptedCookieStorage(SESSION_KEY));A.tasks=asyncio.Queue();A._middlewares.append(session_middleware);A._middlewares.append(auth_middleware);A.jinja2_env.add_extension(MarkdownExtension);A.jinja2_env.add_extension(LinkifyExtension);A.jinja2_env.add_extension(PythonExtension);A.jinja2_env.add_extension(EmojiExtension);A.setup_router();A.executor=_D;A.cache=Cache(A);A.services=get_services(app=A);A.mappers=get_mappers(app=A);A.on_startup.append(A.prepare_asyncio);A.on_startup.append(A.prepare_database)
|
|
||||||
async def prepare_asyncio(A,app):app.executor=ThreadPoolExecutor(max_workers=200);app.loop.set_default_executor(A.executor)
|
def __init__(self, *args, **kwargs):
|
||||||
async def create_task(A,task):await A.tasks.put(task)
|
middlewares = [
|
||||||
async def task_runner(A):
|
cors_middleware,
|
||||||
while _C:
|
web.normalize_path_middleware(merge_slashes=True),
|
||||||
B=await A.tasks.get();A.db.begin()
|
]
|
||||||
try:C=time.time();await B;D=time.time();print(f"Task {B} took {D-C} seconds");A.tasks.task_done()
|
self.template_path = pathlib.Path(__file__).parent.joinpath("templates")
|
||||||
except Exception as E:print(E)
|
self.static_path = pathlib.Path(__file__).parent.joinpath("static")
|
||||||
A.db.commit()
|
super().__init__(
|
||||||
async def prepare_database(A,app):
|
middlewares=middlewares, template_path=self.template_path, client_max_size=1024*1024*1024*5 *args, **kwargs
|
||||||
C='channel_message';D='channel_member';E='username';B='user_uid';A.db.query('PRAGMA journal_mode=WAL');A.db.query('PRAGMA syncnorm=off')
|
)
|
||||||
|
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.time_start = datetime.now()
|
||||||
|
self.ssh_host = "0.0.0.0"
|
||||||
|
self.ssh_port = 2242
|
||||||
|
self.setup_router()
|
||||||
|
self.ssh_server = None
|
||||||
|
self.sync_service = None
|
||||||
|
self.executor = None
|
||||||
|
self.cache = Cache(self)
|
||||||
|
self.services = get_services(app=self)
|
||||||
|
self.mappers = get_mappers(app=self)
|
||||||
|
self.broadcast_service = None
|
||||||
|
self.user_availability_service_task = None
|
||||||
|
|
||||||
|
self.on_startup.append(self.prepare_asyncio)
|
||||||
|
self.on_startup.append(self.start_user_availability_service)
|
||||||
|
self.on_startup.append(self.start_ssh_server)
|
||||||
|
self.on_startup.append(self.prepare_database)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uptime_seconds(self):
|
||||||
|
return (datetime.now() - self.time_start).total_seconds()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uptime(self):
|
||||||
|
return self._format_uptime(self.uptime_seconds)
|
||||||
|
|
||||||
|
def _format_uptime(self,seconds):
|
||||||
|
seconds = int(seconds)
|
||||||
|
days, seconds = divmod(seconds, 86400)
|
||||||
|
hours, seconds = divmod(seconds, 3600)
|
||||||
|
minutes, seconds = divmod(seconds, 60)
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if days > 0:
|
||||||
|
parts.append(f"{days} day{'s' if days != 1 else ''}")
|
||||||
|
if hours > 0:
|
||||||
|
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
|
||||||
|
if minutes > 0:
|
||||||
|
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
|
||||||
|
if seconds > 0 or not parts:
|
||||||
|
parts.append(f"{seconds} second{'s' if seconds != 1 else ''}")
|
||||||
|
|
||||||
|
return ", ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
async def start_user_availability_service(self, app):
|
||||||
|
app.user_availability_service_task = asyncio.create_task(app.services.socket.user_availability_service())
|
||||||
|
async def snode_sync(self, app):
|
||||||
|
self.sync_service = asyncio.create_task(snode.sync_service(app))
|
||||||
|
|
||||||
|
async def start_ssh_server(self, app):
|
||||||
|
app.ssh_server = await start_ssh_server(app,app.ssh_host,app.ssh_port)
|
||||||
|
if app.ssh_server:
|
||||||
|
asyncio.create_task(app.ssh_server.wait_closed())
|
||||||
|
|
||||||
|
async def prepare_asyncio(self, app):
|
||||||
|
# app.loop = asyncio.get_running_loop()
|
||||||
|
app.executor = ThreadPoolExecutor(max_workers=200)
|
||||||
|
app.loop.set_default_executor(self.executor)
|
||||||
|
|
||||||
|
async def create_task(self, task):
|
||||||
|
await self.tasks.put(task)
|
||||||
|
|
||||||
|
async def task_runner(self):
|
||||||
|
while True:
|
||||||
|
task = await self.tasks.get()
|
||||||
|
self.db.begin()
|
||||||
try:
|
try:
|
||||||
if not A.db[_E].has_index(E):A.db[_E].create_index(E,unique=_C)
|
await task
|
||||||
if not A.db[D].has_index([_B,B]):A.db[D].create_index([_B,B])
|
self.tasks.task_done()
|
||||||
if not A.db[C].has_index([_B,B]):A.db[C].create_index([_B,B])
|
except Exception as ex:
|
||||||
except:pass
|
print(ex)
|
||||||
await app.services.drive.prepare_all();A.loop.create_task(A.task_runner())
|
self.db.commit()
|
||||||
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 prepare_database(self, app):
|
||||||
async def handle_http_get(C,request):A=request.query.get('url');B=await http.get(A);return web.Response(body=B)
|
self.db.query("PRAGMA journal_mode=WAL")
|
||||||
async def handle_http_photo(C,request):A=request.query.get('url');B=await http.create_site_photo(A);return web.Response(body=B.read_bytes(),headers={'Content-Type':'image/png'})
|
self.db.query("PRAGMA syncnorm=off")
|
||||||
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=[]
|
try:
|
||||||
if not C:C={}
|
if not self.db["user"].has_index("username"):
|
||||||
C['rid']=str(uuid.uuid4())
|
self.db["user"].create_index("username", unique=True)
|
||||||
if D.session.get(_A):
|
if not self.db["channel_member"].has_index(["channel_uid", "user_uid"]):
|
||||||
async for E in A.services.channel_member.find(user_uid=D.session.get(_A),deleted_at=_D,is_banned=False):
|
self.db["channel_member"].create_index(["channel_uid", "user_uid"])
|
||||||
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 not self.db["channel_message"].has_index(["channel_uid", "user_uid"]):
|
||||||
if N:P=await N.get_user();O=P[K]
|
self.db["channel_message"].create_index(["channel_uid", "user_uid"])
|
||||||
B[K]=O;B[F]=H[F];B['is_private']=H['tag']=='dm'
|
except:
|
||||||
if M:B[_G]=M['nick'];B[_A]=E[_B]
|
pass
|
||||||
else:B[_G]=E['label'];B[_A]=E[_B]
|
|
||||||
B[J]=E[J];G.append(B)
|
await app.services.drive.prepare_all()
|
||||||
G.sort(key=lambda x:x[F]or'',reverse=_C)
|
self.loop.create_task(self.task_runner())
|
||||||
if I not in C:C[I]=G
|
|
||||||
if _E not in C:C[_E]=await A.services.user.get(D.session.get(_A))
|
def setup_router(self):
|
||||||
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
|
self.router.add_get("/", IndexView)
|
||||||
async def static_handler(B,request):
|
self.router.add_static(
|
||||||
D=request;E=D.match_info.get('filename','');C=[];F=D.session.get(_A)
|
"/",
|
||||||
if F:
|
pathlib.Path(__file__).parent.joinpath("static"),
|
||||||
A=await B.services.user.get_static_path(F)
|
name="static",
|
||||||
if A:C.append(A)
|
show_index=True,
|
||||||
for H in B.services.user.get_admin_uids():
|
)
|
||||||
A=await B.services.user.get_static_path(H)
|
self.router.add_view("/profiler.html", profiler_handler)
|
||||||
if A:C.append(A)
|
self.router.add_view("/about.html", AboutHTMLView)
|
||||||
C.append(B.static_path)
|
self.router.add_view("/about.md", AboutMDView)
|
||||||
for G in C:
|
self.router.add_view("/logout.json", LogoutView)
|
||||||
if pathlib.Path(G).joinpath(E).exists():return web.FileResponse(pathlib.Path(G).joinpath(E))
|
self.router.add_view("/logout.html", LogoutView)
|
||||||
return web.HTTPNotFound()
|
self.router.add_view("/docs.html", DocsHTMLView)
|
||||||
async def get_user_template_loader(B,uid=_D):
|
self.router.add_view("/docs.md", DocsMDView)
|
||||||
C=[]
|
self.router.add_view("/status.json", StatusView)
|
||||||
for D in B.services.user.get_admin_uids():
|
self.router.add_view("/settings/index.html", SettingsIndexView)
|
||||||
A=await B.services.user.get_template_path(D)
|
self.router.add_view("/settings/profile.html", SettingsProfileView)
|
||||||
if A:C.append(A)
|
self.router.add_view("/settings/profile.json", SettingsProfileView)
|
||||||
|
self.router.add_view("/web.html", WebView)
|
||||||
|
self.router.add_view("/login.html", LoginView)
|
||||||
|
self.router.add_view("/login.json", LoginView)
|
||||||
|
self.router.add_view("/register.html", RegisterView)
|
||||||
|
self.router.add_view("/register.json", RegisterView)
|
||||||
|
self.router.add_view("/drive/{rel_path:.*}", DriveView)
|
||||||
|
self.router.add_view("/drive.bin", UploadView)
|
||||||
|
self.router.add_view("/drive.bin/{uid}.{ext}", UploadView)
|
||||||
|
self.router.add_view("/search-user.html", SearchUserView)
|
||||||
|
self.router.add_view("/search-user.json", SearchUserView)
|
||||||
|
self.router.add_view("/avatar/{uid}.svg", AvatarView)
|
||||||
|
self.router.add_get("/http-get", self.handle_http_get)
|
||||||
|
self.router.add_get("/http-photo", self.handle_http_photo)
|
||||||
|
self.router.add_get("/rpc.ws", RPCView)
|
||||||
|
self.router.add_get("/c/{channel:.*}", ChannelView)
|
||||||
|
self.router.add_view("/channel/{channel_uid}/attachment.bin",ChannelAttachmentView)
|
||||||
|
self.router.add_view("/channel/attachment/{relative_url:.*}",ChannelAttachmentView)
|
||||||
|
self.router.add_view("/channel/{channel}.html", WebView)
|
||||||
|
self.router.add_view("/threads.html", ThreadsView)
|
||||||
|
self.router.add_view("/terminal.ws", TerminalSocketView)
|
||||||
|
self.router.add_view("/terminal.html", TerminalView)
|
||||||
|
self.router.add_view("/drive.json", DriveApiView)
|
||||||
|
self.router.add_view("/drive.html", DriveView)
|
||||||
|
self.router.add_view("/drive/{drive}.json", DriveView)
|
||||||
|
self.router.add_view("/stats.json", StatsView)
|
||||||
|
self.router.add_view("/user/{user}.html", UserView)
|
||||||
|
self.router.add_view("/repository/{username}/{repository}", RepositoryView)
|
||||||
|
self.router.add_view("/repository/{username}/{repository}/{path:.*}", RepositoryView)
|
||||||
|
self.router.add_view("/settings/repositories/index.html", RepositoriesIndexView)
|
||||||
|
self.router.add_view("/settings/repositories/create.html", RepositoriesCreateView)
|
||||||
|
self.router.add_view("/settings/repositories/repository/{name}/update.html", RepositoriesUpdateView)
|
||||||
|
self.router.add_view("/settings/repositories/repository/{name}/delete.html", RepositoriesDeleteView)
|
||||||
|
self.router.add_view("/settings/containers/index.html", ContainersIndexView)
|
||||||
|
self.router.add_view("/settings/containers/create.html", ContainersCreateView)
|
||||||
|
self.router.add_view("/settings/containers/container/{uid}/update.html", ContainersUpdateView)
|
||||||
|
self.router.add_view("/settings/containers/container/{uid}/delete.html", ContainersDeleteView)
|
||||||
|
self.webdav = WebdavApplication(self)
|
||||||
|
self.git = GitApplication(self)
|
||||||
|
self.add_subapp("/webdav", self.webdav)
|
||||||
|
self.add_subapp("/git",self.git)
|
||||||
|
|
||||||
|
#self.router.add_get("/{file_path:.*}", self.static_handler)
|
||||||
|
|
||||||
|
async def handle_test(self, request):
|
||||||
|
|
||||||
|
return await self.render_template(
|
||||||
|
"test.html", request, context={"name": "retoor"}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle_http_get(self, request: web.Request):
|
||||||
|
url = request.query.get("url")
|
||||||
|
content = await http.get(url)
|
||||||
|
return web.Response(body=content)
|
||||||
|
|
||||||
|
async def handle_http_photo(self, request):
|
||||||
|
url = request.query.get("url")
|
||||||
|
path = await http.create_site_photo(url)
|
||||||
|
return web.Response(
|
||||||
|
body=path.read_bytes(), headers={"Content-Type": "image/png"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# @time_cache_async(60)
|
||||||
|
async def render_template(self, template, request, context=None):
|
||||||
|
channels = []
|
||||||
|
if not context:
|
||||||
|
context = {}
|
||||||
|
context["rid"] = str(uuid.uuid4())
|
||||||
|
if request.session.get("uid"):
|
||||||
|
async for subscribed_channel in self.services.channel_member.find(
|
||||||
|
user_uid=request.session.get("uid"), deleted_at=None, is_banned=False
|
||||||
|
):
|
||||||
|
|
||||||
|
parent_object = await subscribed_channel.get_channel()
|
||||||
|
|
||||||
|
item = {}
|
||||||
|
other_user = await self.services.channel_member.get_other_dm_user(
|
||||||
|
subscribed_channel["channel_uid"], request.session.get("uid")
|
||||||
|
)
|
||||||
|
|
||||||
|
last_message = await parent_object.get_last_message()
|
||||||
|
color = None
|
||||||
|
if last_message:
|
||||||
|
last_message_user = await last_message.get_user()
|
||||||
|
color = last_message_user["color"]
|
||||||
|
item["color"] = color
|
||||||
|
item["last_message_on"] = parent_object["last_message_on"]
|
||||||
|
item["is_private"] = parent_object["tag"] == "dm"
|
||||||
|
if other_user:
|
||||||
|
item["name"] = other_user["nick"]
|
||||||
|
item["uid"] = subscribed_channel["channel_uid"]
|
||||||
|
else:
|
||||||
|
item["name"] = subscribed_channel["label"]
|
||||||
|
item["uid"] = subscribed_channel["channel_uid"]
|
||||||
|
item["new_count"] = subscribed_channel["new_count"]
|
||||||
|
|
||||||
|
channels.append(item)
|
||||||
|
|
||||||
|
channels.sort(key=lambda x: x["last_message_on"] or "", reverse=True)
|
||||||
|
if "channels" not in context:
|
||||||
|
context["channels"] = channels
|
||||||
|
if "user" not in context:
|
||||||
|
context["user"] = await self.services.user.get(
|
||||||
|
request.session.get("uid")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.template_path.joinpath(template)
|
||||||
|
|
||||||
|
await self.services.user.get_template_path(request.session.get("uid"))
|
||||||
|
|
||||||
|
self.original_loader = self.jinja2_env.loader
|
||||||
|
|
||||||
|
self.jinja2_env.loader = await self.get_user_template_loader(
|
||||||
|
request.session.get("uid")
|
||||||
|
)
|
||||||
|
|
||||||
|
rendered = await super().render_template(template, request, context)
|
||||||
|
|
||||||
|
self.jinja2_env.loader = self.original_loader
|
||||||
|
|
||||||
|
return rendered
|
||||||
|
|
||||||
|
|
||||||
|
async def static_handler(self, request):
|
||||||
|
file_name = request.match_info.get('filename', '')
|
||||||
|
|
||||||
|
paths = []
|
||||||
|
|
||||||
|
uid = request.session.get("uid")
|
||||||
if uid:
|
if uid:
|
||||||
A=await B.services.user.get_template_path(uid)
|
user_static_path = await self.services.user.get_static_path(uid)
|
||||||
if A:C.append(A)
|
if user_static_path:
|
||||||
C.append(B.template_path);return FileSystemLoader(C)
|
paths.append(user_static_path)
|
||||||
app=Application(db_path='sqlite:///snek.db')
|
|
||||||
async def main():await web._run_app(app,port=8081,host='0.0.0.0')
|
for admin_uid in self.services.user.get_admin_uids():
|
||||||
if __name__=='__main__':asyncio.run(main())
|
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())
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class LoadBalancer:
|
||||||
|
def __init__(self, backend_ports):
|
||||||
|
self.backend_ports = backend_ports
|
||||||
|
self.backend_processes = []
|
||||||
|
self.client_counts = [0] * len(backend_ports)
|
||||||
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def start_backend_servers(self,port,workers):
|
||||||
|
for x in range(workers):
|
||||||
|
port += 1
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
sys.executable,
|
||||||
|
sys.argv[0],
|
||||||
|
'backend',
|
||||||
|
str(port),
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
port += 1
|
||||||
|
self.backend_processes.append(process)
|
||||||
|
print(f"Started backend server on port {(port-1)/port} with PID {process.pid}")
|
||||||
|
|
||||||
|
async def handle_client(self, reader, writer):
|
||||||
|
async with self.lock:
|
||||||
|
min_clients = min(self.client_counts)
|
||||||
|
server_index = self.client_counts.index(min_clients)
|
||||||
|
self.client_counts[server_index] += 1
|
||||||
|
backend = ('127.0.0.1', self.backend_ports[server_index])
|
||||||
|
try:
|
||||||
|
backend_reader, backend_writer = await asyncio.open_connection(*backend)
|
||||||
|
|
||||||
|
async def forward(r, w):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await r.read(1024)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
w.write(data)
|
||||||
|
await w.drain()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
w.close()
|
||||||
|
|
||||||
|
task1 = asyncio.create_task(forward(reader, backend_writer))
|
||||||
|
task2 = asyncio.create_task(forward(backend_reader, writer))
|
||||||
|
await asyncio.gather(task1, task2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
finally:
|
||||||
|
writer.close()
|
||||||
|
async with self.lock:
|
||||||
|
self.client_counts[server_index] -= 1
|
||||||
|
|
||||||
|
async def monitor(self):
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
print("Connected clients per server:")
|
||||||
|
for i, count in enumerate(self.client_counts):
|
||||||
|
print(f"Server {self.backend_ports[i]}: {count} clients")
|
||||||
|
|
||||||
|
async def start(self, host='0.0.0.0', port=8081,workers=5):
|
||||||
|
await self.start_backend_servers(port,workers)
|
||||||
|
server = await asyncio.start_server(self.handle_client, host, port)
|
||||||
|
monitor_task = asyncio.create_task(self.monitor())
|
||||||
|
|
||||||
|
# Handle shutdown gracefully
|
||||||
|
try:
|
||||||
|
async with server:
|
||||||
|
await server.serve_forever()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
# Terminate backend processes
|
||||||
|
for process in self.backend_processes:
|
||||||
|
process.terminate()
|
||||||
|
await asyncio.gather(*(p.wait() for p in self.backend_processes))
|
||||||
|
print("Backend processes terminated.")
|
||||||
|
|
||||||
|
async def backend_echo_server(port):
|
||||||
|
async def handle_echo(reader, writer):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await reader.read(1024)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
writer.write(data)
|
||||||
|
await writer.drain()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
writer.close()
|
||||||
|
|
||||||
|
server = await asyncio.start_server(handle_echo, '127.0.0.1', port)
|
||||||
|
print(f"Backend echo server running on port {port}")
|
||||||
|
await server.serve_forever()
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
backend_ports = [8001, 8003, 8005, 8006]
|
||||||
|
# Launch backend echo servers
|
||||||
|
# Wait a moment for servers to start
|
||||||
|
lb = LoadBalancer(backend_ports)
|
||||||
|
await lb.start()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
if sys.argv[1] == 'backend':
|
||||||
|
port = int(sys.argv[2])
|
||||||
|
from snek.app import Application
|
||||||
|
snek = Application(port=port)
|
||||||
|
web.run_app(snek, port=port, host='127.0.0.1')
|
||||||
|
elif sys.argv[1] == 'sync':
|
||||||
|
from snek.sync import app
|
||||||
|
web.run_app(snek, port=port, host='127.0.0.1')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Shutting down...")
|
||||||
|
|
||||||
+38
-9
@@ -1,14 +1,43 @@
|
|||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from app.app import Application as BaseApplication
|
from app.app import Application as BaseApplication
|
||||||
|
|
||||||
from snek.system.markdown import MarkdownExtension
|
from snek.system.markdown import MarkdownExtension
|
||||||
|
|
||||||
|
|
||||||
class Application(BaseApplication):
|
class Application(BaseApplication):
|
||||||
def __init__(A,path=None,*B,**C):A.path=pathlib.Path(path);D=A.path;super().__init__(*B,template_path=D,**C);A.jinja2_env.add_extension(MarkdownExtension);A.router.add_get('/{tail:.*}',A.handle_document)
|
|
||||||
async def handle_document(B,request):
|
def __init__(self, path=None, *args, **kwargs):
|
||||||
D='text/plain';E=b'Resource is not found on this server.';F='index.html';G=request;C=G.match_info['tail'].strip('/')
|
self.path = pathlib.Path(path)
|
||||||
if C=='':C=F
|
template_path = self.path
|
||||||
A=B.path.joinpath(C)
|
|
||||||
if not A.exists():return web.Response(status=404,body=E,content_type=D)
|
super().__init__(template_path=template_path, *args, **kwargs)
|
||||||
if A.is_dir():A=A.joinpath(F)
|
self.jinja2_env.add_extension(MarkdownExtension)
|
||||||
if not A.exists():return web.Response(status=404,body=E,content_type=D)
|
|
||||||
H=await B.render_template(str(A.relative_to(B.path)),G);return H
|
self.router.add_get("/{tail:.*}", self.handle_document)
|
||||||
|
|
||||||
|
async def handle_document(self, request):
|
||||||
|
relative_path = request.match_info["tail"].strip("/")
|
||||||
|
if relative_path == "":
|
||||||
|
relative_path = "index.html"
|
||||||
|
document_path = self.path.joinpath(relative_path)
|
||||||
|
if not document_path.exists():
|
||||||
|
return web.Response(
|
||||||
|
status=404,
|
||||||
|
body=b"Resource is not found on this server.",
|
||||||
|
content_type="text/plain",
|
||||||
|
)
|
||||||
|
if document_path.is_dir():
|
||||||
|
document_path = document_path.joinpath("index.html")
|
||||||
|
if not document_path.exists():
|
||||||
|
return web.Response(
|
||||||
|
status=404,
|
||||||
|
body=b"Resource is not found on this server.",
|
||||||
|
content_type="text/plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await self.render_template(
|
||||||
|
str(document_path.relative_to(self.path)), request
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|||||||
+36
-9
@@ -1,12 +1,39 @@
|
|||||||
_B='created_at'
|
|
||||||
_A='uid'
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from snek.app import app
|
from snek.app import app
|
||||||
async def fix_message(message):C='user';D='text';B='user_uid';A=message;A={_A:A[_A],B:A[B],D:A['message'],'sent':A[_B]};E=await app.services.user.get(uid=A[B]);A[C]=E and E['username']or None;return(A[C]or'')+': '+(A[D]or'')
|
|
||||||
|
|
||||||
|
async def fix_message(message):
|
||||||
|
message = {
|
||||||
|
"uid": message["uid"],
|
||||||
|
"user_uid": message["user_uid"],
|
||||||
|
"text": message["message"],
|
||||||
|
"sent": message["created_at"],
|
||||||
|
}
|
||||||
|
user = await app.services.user.get(uid=message["user_uid"])
|
||||||
|
message["user"] = user and user["username"] or None
|
||||||
|
return (message["user"] or "") + ": " + (message["text"] or "")
|
||||||
|
|
||||||
|
|
||||||
async def dump_public_channels():
|
async def dump_public_channels():
|
||||||
A=[]
|
result = []
|
||||||
for B in app.db['channel'].find(is_private=False,is_listed=True,tag='public'):print(f"Dumping channel: {B["label"]}.");A+=[await fix_message(A)for A in app.db['channel_message'].find(channel_uid=B[_A],order_by=_B)];print('Dump succesfull!')
|
for channel in app.db["channel"].find(
|
||||||
print('Converting to json.');print('Converting succesful, now writing to dump.json')
|
is_private=False, is_listed=True, tag="public"
|
||||||
with open('dump.txt','w')as C:C.write('\n\n'.join(A))
|
):
|
||||||
print('Dump written to dump.json')
|
print(f"Dumping channel: {channel['label']}.")
|
||||||
if __name__=='__main__':asyncio.run(dump_public_channels())
|
result += [
|
||||||
|
await fix_message(record)
|
||||||
|
for record in app.db["channel_message"].find(
|
||||||
|
channel_uid=channel["uid"], order_by="created_at"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
print("Dump succesfull!")
|
||||||
|
print("Converting to json.")
|
||||||
|
print("Converting succesful, now writing to dump.json")
|
||||||
|
with open("dump.txt", "w") as f:
|
||||||
|
f.write("\n\n".join(result))
|
||||||
|
print("Dump written to dump.json")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(dump_public_channels())
|
||||||
|
|||||||
+45
-8
@@ -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):
|
class AuthField(FormInputElement):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
async def errors(self):
|
async def errors(self):
|
||||||
A=self;B=await super().errors
|
result = await super().errors
|
||||||
if A.model.password.value and A.model.username.value:
|
if self.model.password.value and self.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']
|
if not await self.app.services.user.validate_login(
|
||||||
return B
|
self.model.username.value, self.model.password.value
|
||||||
|
):
|
||||||
|
return ["Invalid username or password"]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(Form):
|
class LoginForm(Form):
|
||||||
title=HTMLElement(tag='h1',text='Login');username=AuthField(name=_B,required=True,min_length=2,max_length=20,regex='^[a-zA-Z0-9_-]+$',place_holder='Username',type='text');password=AuthField(name=_A,required=True,min_length=1,type=_A,place_holder='Password');action=FormButtonElement(name='action',value='submit',text='Login',type='button')
|
|
||||||
|
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
|
@property
|
||||||
async def is_valid(self):A=self;return all([A[_B],A[_A],not await A.username.errors,not await A.password.errors])
|
async def is_valid(self):
|
||||||
|
return all(
|
||||||
|
[
|
||||||
|
self["username"],
|
||||||
|
self["password"],
|
||||||
|
not await self.username.errors,
|
||||||
|
not await self.password.errors,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|||||||
@@ -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):
|
class UsernameField(FormInputElement):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
async def errors(self):
|
async def errors(self):
|
||||||
A=self;B=await super().errors
|
result = await super().errors
|
||||||
if A.value and await A.app.services.user.count(username=A.value):B.append('Username is not available.')
|
if self.value and await self.app.services.user.count(username=self.value):
|
||||||
return B
|
result.append("Username is not available.")
|
||||||
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')
|
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"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,2 +1,18 @@
|
|||||||
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
|
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
|
||||||
class SearchUserForm(Form):title=HTMLElement(tag='h1',text='Search user');username=FormInputElement(name='username',required=True,min_length=1,max_length=128,place_holder='Username');action=FormButtonElement(name='action',value='submit',text='Search',type='button')
|
|
||||||
|
|
||||||
|
class SearchUserForm(Form):
|
||||||
|
|
||||||
|
title = HTMLElement(tag="h1", text="Search user")
|
||||||
|
|
||||||
|
username = FormInputElement(
|
||||||
|
name="username",
|
||||||
|
required=True,
|
||||||
|
min_length=1,
|
||||||
|
max_length=128,
|
||||||
|
place_holder="Username",
|
||||||
|
)
|
||||||
|
|
||||||
|
action = FormButtonElement(
|
||||||
|
name="action", value="submit", text="Search", type="button"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
_C='button'
|
|
||||||
_B='submit'
|
|
||||||
_A='action'
|
|
||||||
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
|
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)
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
from snek.app import app
|
from snek.app import app
|
||||||
|
|
||||||
application = app
|
application = app
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import functools
|
import functools
|
||||||
|
|
||||||
from snek.mapper.channel import ChannelMapper
|
from snek.mapper.channel import ChannelMapper
|
||||||
from snek.mapper.channel_member import ChannelMemberMapper
|
from snek.mapper.channel_member import ChannelMemberMapper
|
||||||
from snek.mapper.channel_message import ChannelMessageMapper
|
from snek.mapper.channel_message import ChannelMessageMapper
|
||||||
@@ -8,7 +9,30 @@ from snek.mapper.notification import NotificationMapper
|
|||||||
from snek.mapper.user import UserMapper
|
from snek.mapper.user import UserMapper
|
||||||
from snek.mapper.user_property import UserPropertyMapper
|
from snek.mapper.user_property import UserPropertyMapper
|
||||||
from snek.mapper.repository import RepositoryMapper
|
from snek.mapper.repository import RepositoryMapper
|
||||||
|
from snek.mapper.channel_attachment import ChannelAttachmentMapper
|
||||||
|
from snek.mapper.container import ContainerMapper
|
||||||
from snek.system.object import Object
|
from snek.system.object import Object
|
||||||
|
|
||||||
|
|
||||||
@functools.cache
|
@functools.cache
|
||||||
def get_mappers(app=None):A=app;return Object(**{'user':UserMapper(app=A),'channel_member':ChannelMemberMapper(app=A),'channel':ChannelMapper(app=A),'channel_message':ChannelMessageMapper(app=A),'notification':NotificationMapper(app=A),'drive_item':DriveItemMapper(app=A),'drive':DriveMapper(app=A),'user_property':UserPropertyMapper(app=A),'repository':RepositoryMapper(app=A)})
|
|
||||||
def get_mapper(name,app=None):return get_mappers(app=app)[name]
|
def get_mappers(app=None):
|
||||||
|
return Object(
|
||||||
|
**{
|
||||||
|
"user": UserMapper(app=app),
|
||||||
|
"channel_member": ChannelMemberMapper(app=app),
|
||||||
|
"channel": ChannelMapper(app=app),
|
||||||
|
"channel_message": ChannelMessageMapper(app=app),
|
||||||
|
"notification": NotificationMapper(app=app),
|
||||||
|
"drive_item": DriveItemMapper(app=app),
|
||||||
|
"drive": DriveMapper(app=app),
|
||||||
|
"user_property": UserPropertyMapper(app=app),
|
||||||
|
"repository": RepositoryMapper(app=app),
|
||||||
|
"channel_attachment": ChannelAttachmentMapper(app=app),
|
||||||
|
"container": ContainerMapper(app=app),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_mapper(name, app=None):
|
||||||
|
return get_mappers(app=app)[name]
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
from snek.model.channel import ChannelModel
|
from snek.model.channel import ChannelModel
|
||||||
from snek.system.mapper import BaseMapper
|
from snek.system.mapper import BaseMapper
|
||||||
class ChannelMapper(BaseMapper):table_name='channel';model_class=ChannelModel
|
|
||||||
|
|
||||||
|
class ChannelMapper(BaseMapper):
|
||||||
|
table_name = "channel"
|
||||||
|
model_class = ChannelModel
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from snek.model.channel_attachment import ChannelAttachmentModel
|
||||||
|
from snek.system.mapper import BaseMapper
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelAttachmentMapper(BaseMapper):
|
||||||
|
table_name = "channel_attachment"
|
||||||
|
model_class = ChannelAttachmentModel
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
from snek.model.channel_member import ChannelMemberModel
|
from snek.model.channel_member import ChannelMemberModel
|
||||||
from snek.system.mapper import BaseMapper
|
from snek.system.mapper import BaseMapper
|
||||||
class ChannelMemberMapper(BaseMapper):table_name='channel_member';model_class=ChannelMemberModel
|
|
||||||
|
|
||||||
|
class ChannelMemberMapper(BaseMapper):
|
||||||
|
table_name = "channel_member"
|
||||||
|
model_class = ChannelMemberModel
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
from snek.model.channel_message import ChannelMessageModel
|
from snek.model.channel_message import ChannelMessageModel
|
||||||
from snek.system.mapper import BaseMapper
|
from snek.system.mapper import BaseMapper
|
||||||
class ChannelMessageMapper(BaseMapper):model_class=ChannelMessageModel;table_name='channel_message'
|
|
||||||
|
|
||||||
|
class ChannelMessageMapper(BaseMapper):
|
||||||
|
model_class = ChannelMessageModel
|
||||||
|
table_name = "channel_message"
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from snek.model.container import Container
|
||||||
|
from snek.system.mapper import BaseMapper
|
||||||
|
|
||||||
|
class ContainerMapper(BaseMapper):
|
||||||
|
model_class = Container
|
||||||
|
table_name = "container"
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
from snek.model.drive import DriveModel
|
from snek.model.drive import DriveModel
|
||||||
from snek.system.mapper import BaseMapper
|
from snek.system.mapper import BaseMapper
|
||||||
class DriveMapper(BaseMapper):table_name='drive';model_class=DriveModel
|
|
||||||
|
|
||||||
|
class DriveMapper(BaseMapper):
|
||||||
|
table_name = "drive"
|
||||||
|
model_class = DriveModel
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
from snek.model.drive_item import DriveItemModel
|
from snek.model.drive_item import DriveItemModel
|
||||||
from snek.system.mapper import BaseMapper
|
from snek.system.mapper import BaseMapper
|
||||||
class DriveItemMapper(BaseMapper):model_class=DriveItemModel;table_name='drive_item'
|
|
||||||
|
|
||||||
|
class DriveItemMapper(BaseMapper):
|
||||||
|
|
||||||
|
model_class = DriveItemModel
|
||||||
|
table_name = "drive_item"
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
from snek.model.notification import NotificationModel
|
from snek.model.notification import NotificationModel
|
||||||
from snek.system.mapper import BaseMapper
|
from snek.system.mapper import BaseMapper
|
||||||
class NotificationMapper(BaseMapper):table_name='notification';model_class=NotificationModel
|
|
||||||
|
|
||||||
|
class NotificationMapper(BaseMapper):
|
||||||
|
table_name = "notification"
|
||||||
|
model_class = NotificationModel
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
from snek.model.repository import RepositoryModel
|
from snek.model.repository import RepositoryModel
|
||||||
from snek.system.mapper import BaseMapper
|
from snek.system.mapper import BaseMapper
|
||||||
class RepositoryMapper(BaseMapper):model_class=RepositoryModel;table_name='repository'
|
|
||||||
|
|
||||||
|
class RepositoryMapper(BaseMapper):
|
||||||
|
model_class = RepositoryModel
|
||||||
|
table_name = "repository"
|
||||||
|
|||||||
+17
-4
@@ -1,7 +1,20 @@
|
|||||||
from snek.model.user import UserModel
|
from snek.model.user import UserModel
|
||||||
from snek.system.mapper import BaseMapper
|
from snek.system.mapper import BaseMapper
|
||||||
|
|
||||||
|
|
||||||
class UserMapper(BaseMapper):
|
class UserMapper(BaseMapper):
|
||||||
table_name='user';model_class=UserModel
|
table_name = "user"
|
||||||
def get_admin_uids(A):
|
model_class = UserModel
|
||||||
try:return[A['uid']for A in A.db.query('SELECT uid FROM user WHERE is_admin = :is_admin',{'is_admin':True})]
|
|
||||||
except Exception as B:print(B);return[]
|
def get_admin_uids(self):
|
||||||
|
try:
|
||||||
|
return [
|
||||||
|
user["uid"]
|
||||||
|
for user in self.db.query(
|
||||||
|
"SELECT uid FROM user WHERE is_admin = :is_admin",
|
||||||
|
{"is_admin": True},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
except Exception as ex:
|
||||||
|
print(ex)
|
||||||
|
return []
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
from snek.model.user_property import UserPropertyModel
|
from snek.model.user_property import UserPropertyModel
|
||||||
from snek.system.mapper import BaseMapper
|
from snek.system.mapper import BaseMapper
|
||||||
class UserPropertyMapper(BaseMapper):table_name='user_property';model_class=UserPropertyModel
|
|
||||||
|
|
||||||
|
class UserPropertyMapper(BaseMapper):
|
||||||
|
table_name = "user_property"
|
||||||
|
model_class = UserPropertyModel
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import functools
|
import functools
|
||||||
|
|
||||||
from snek.model.channel import ChannelModel
|
from snek.model.channel import ChannelModel
|
||||||
from snek.model.channel_member import ChannelMemberModel
|
from snek.model.channel_member import ChannelMemberModel
|
||||||
|
|
||||||
|
# from snek.model.channel_message import ChannelMessageModel
|
||||||
from snek.model.channel_message import ChannelMessageModel
|
from snek.model.channel_message import ChannelMessageModel
|
||||||
from snek.model.drive import DriveModel
|
from snek.model.drive import DriveModel
|
||||||
from snek.model.drive_item import DriveItemModel
|
from snek.model.drive_item import DriveItemModel
|
||||||
@@ -8,7 +11,29 @@ from snek.model.notification import NotificationModel
|
|||||||
from snek.model.user import UserModel
|
from snek.model.user import UserModel
|
||||||
from snek.model.user_property import UserPropertyModel
|
from snek.model.user_property import UserPropertyModel
|
||||||
from snek.model.repository import RepositoryModel
|
from snek.model.repository import RepositoryModel
|
||||||
|
from snek.model.channel_attachment import ChannelAttachmentModel
|
||||||
|
from snek.model.container import Container
|
||||||
from snek.system.object import Object
|
from snek.system.object import Object
|
||||||
|
|
||||||
|
|
||||||
@functools.cache
|
@functools.cache
|
||||||
def get_models():return Object(**{'user':UserModel,'channel_member':ChannelMemberModel,'channel':ChannelModel,'channel_message':ChannelMessageModel,'drive_item':DriveItemModel,'drive':DriveModel,'notification':NotificationModel,'user_property':UserPropertyModel,'repository':RepositoryModel})
|
def get_models():
|
||||||
def get_model(name):return get_models()[name]
|
return Object(
|
||||||
|
**{
|
||||||
|
"user": UserModel,
|
||||||
|
"channel_member": ChannelMemberModel,
|
||||||
|
"channel": ChannelModel,
|
||||||
|
"channel_message": ChannelMessageModel,
|
||||||
|
"drive_item": DriveItemModel,
|
||||||
|
"drive": DriveModel,
|
||||||
|
"notification": NotificationModel,
|
||||||
|
"user_property": UserPropertyModel,
|
||||||
|
"repository": RepositoryModel,
|
||||||
|
"channel_attachment": ChannelAttachmentModel,
|
||||||
|
"container": Container,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_model(name):
|
||||||
|
return get_models()[name]
|
||||||
|
|||||||
@@ -1,12 +1,30 @@
|
|||||||
_C='uid'
|
|
||||||
_B=False
|
|
||||||
_A=True
|
|
||||||
from snek.model.channel_message import ChannelMessageModel
|
from snek.model.channel_message import ChannelMessageModel
|
||||||
from snek.system.model import BaseModel, ModelField
|
from snek.system.model import BaseModel, ModelField
|
||||||
|
|
||||||
|
|
||||||
class ChannelModel(BaseModel):
|
class ChannelModel(BaseModel):
|
||||||
label=ModelField(name='label',required=_A,kind=str);description=ModelField(name='description',required=_B,kind=str);tag=ModelField(name='tag',required=_B,kind=str);created_by_uid=ModelField(name='created_by_uid',required=_A,kind=str);is_private=ModelField(name='is_private',required=_A,kind=bool,value=_B);is_listed=ModelField(name='is_listed',required=_A,kind=bool,value=_A);index=ModelField(name='index',required=_A,kind=int,value=1000);last_message_on=ModelField(name='last_message_on',required=_B,kind=str)
|
label = ModelField(name="label", required=True, kind=str)
|
||||||
async def get_last_message(A):
|
description = ModelField(name="description", required=False, kind=str)
|
||||||
|
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:
|
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])
|
async for model in self.app.services.channel_message.query(
|
||||||
except:pass
|
"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1",
|
||||||
async def get_members(A):return await A.app.services.channel_member.find(channel_uid=A[_C],deleted_at=None,is_banned=_B)
|
{"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
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from snek.system.model import BaseModel
|
||||||
|
from snek.system.model import BaseModel, ModelField
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelAttachmentModel(BaseModel):
|
||||||
|
|
||||||
|
name = ModelField(name="name", required=True, kind=str)
|
||||||
|
channel_uid = ModelField(name="channel_uid", required=True, kind=str)
|
||||||
|
path = ModelField(name="path", required=True, kind=str)
|
||||||
|
size = ModelField(name="size", required=False, kind=int)
|
||||||
|
user_uid = ModelField(name="user_uid", required=True, kind=str)
|
||||||
|
mime_type = ModelField(name="type", required=True, kind=str)
|
||||||
|
relative_url = ModelField(name="relative_url", required=True, kind=str)
|
||||||
|
resource_type = ModelField(name="resource_type", required=True, kind=str,value="file")
|
||||||
|
|
||||||
|
|
||||||
@@ -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):
|
class ChannelMemberModel(BaseModel):
|
||||||
label=ModelField(name='label',required=_A,kind=str);channel_uid=ModelField(name=_D,required=_A,kind=str);user_uid=ModelField(name=_C,required=_A,kind=str);is_moderator=ModelField(name='is_moderator',required=_A,kind=bool,value=_B);is_read_only=ModelField(name='is_read_only',required=_A,kind=bool,value=_B);is_muted=ModelField(name='is_muted',required=_A,kind=bool,value=_B);is_banned=ModelField(name='is_banned',required=_A,kind=bool,value=_B);new_count=ModelField(name='new_count',required=_B,kind=int,value=0)
|
label = ModelField(name="label", required=True, kind=str)
|
||||||
async def get_user(A):return await A.app.services.user.get(uid=A[_C])
|
channel_uid = ModelField(name="channel_uid", required=True, kind=str)
|
||||||
async def get_channel(A):return await A.app.services.channel.get(uid=A[_D])
|
user_uid = ModelField(name="user_uid", required=True, kind=str)
|
||||||
async def get_name(A):
|
is_moderator = ModelField(
|
||||||
B=await A.get_channel()
|
name="is_moderator", required=True, kind=bool, value=False
|
||||||
if B['tag']=='dm':C=await A.get_other_dm_user();return C['nick']
|
)
|
||||||
return B['name']or A['label']
|
is_read_only = ModelField(
|
||||||
async def get_other_dm_user(A):
|
name="is_read_only", required=True, kind=bool, value=False
|
||||||
B='uid';C=await A.get_channel()
|
)
|
||||||
if C['tag']!='dm':return
|
is_muted = ModelField(name="is_muted", required=True, kind=bool, value=False)
|
||||||
async for D in A.app.services.channel_member.find(channel_uid=C[B]):
|
is_banned = ModelField(name="is_banned", required=True, kind=bool, value=False)
|
||||||
if D[B]!=A[B]:return await A.app.services.user.get(uid=D[_C])
|
new_count = ModelField(name="new_count", required=False, kind=int, value=0)
|
||||||
return await A.get_user()
|
|
||||||
|
async def get_user(self):
|
||||||
|
return await self.app.services.user.get(uid=self["user_uid"])
|
||||||
|
|
||||||
|
async def get_channel(self):
|
||||||
|
return await self.app.services.channel.get(uid=self["channel_uid"])
|
||||||
|
|
||||||
|
async def get_name(self):
|
||||||
|
channel = await self.get_channel()
|
||||||
|
if channel["tag"] == "dm":
|
||||||
|
user = await self.get_other_dm_user()
|
||||||
|
return user["nick"]
|
||||||
|
return channel["name"] or self["label"]
|
||||||
|
|
||||||
|
async def get_other_dm_user(self):
|
||||||
|
channel = await self.get_channel()
|
||||||
|
if channel["tag"] != "dm":
|
||||||
|
return None
|
||||||
|
|
||||||
|
async for model in self.app.services.channel_member.find(
|
||||||
|
channel_uid=channel["uid"]
|
||||||
|
):
|
||||||
|
if model["uid"] != self["uid"]:
|
||||||
|
return await self.app.services.user.get(uid=model["user_uid"])
|
||||||
|
return await self.get_user()
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
_B='user_uid'
|
|
||||||
_A='channel_uid'
|
|
||||||
from snek.model.user import UserModel
|
from snek.model.user import UserModel
|
||||||
from snek.system.model import BaseModel, ModelField
|
from snek.system.model import BaseModel, ModelField
|
||||||
|
from datetime import datetime,timezone
|
||||||
|
|
||||||
class ChannelMessageModel(BaseModel):
|
class ChannelMessageModel(BaseModel):
|
||||||
channel_uid=ModelField(name=_A,required=True,kind=str);user_uid=ModelField(name=_B,required=True,kind=str);message=ModelField(name='message',required=True,kind=str);html=ModelField(name='html',required=False,kind=str)
|
channel_uid = ModelField(name="channel_uid", required=True, kind=str)
|
||||||
async def get_user(A):return await A.app.services.user.get(uid=A[_B])
|
user_uid = ModelField(name="user_uid", required=True, kind=str)
|
||||||
async def get_channel(A):return await A.app.services.channel.get(uid=A[_A])
|
message = ModelField(name="message", required=True, kind=str)
|
||||||
|
html = ModelField(name="html", required=False, kind=str)
|
||||||
|
is_final = ModelField(name="is_final", required=True, kind=bool, value=True)
|
||||||
|
|
||||||
|
def get_seconds_since_last_update(self):
|
||||||
|
return int((datetime.now(timezone.utc) - datetime.fromisoformat(self["updated_at"])).total_seconds())
|
||||||
|
|
||||||
|
async def get_user(self) -> UserModel:
|
||||||
|
return await self.app.services.user.get(uid=self["user_uid"])
|
||||||
|
|
||||||
|
async def get_channel(self):
|
||||||
|
return await self.app.services.channel.get(uid=self["channel_uid"])
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from snek.system.model import BaseModel, ModelField
|
||||||
|
|
||||||
|
class Container(BaseModel):
|
||||||
|
id = ModelField(name="id", required=True, kind=str)
|
||||||
|
name = ModelField(name="name", required=True, kind=str)
|
||||||
|
status = ModelField(name="status", required=True, kind=str)
|
||||||
|
resources = ModelField(name="resources", required=False, kind=str)
|
||||||
|
user_uid = ModelField(name="user_uid", required=False, kind=str)
|
||||||
|
path = ModelField(name="path", required=False, kind=str)
|
||||||
|
readonly = ModelField(name="readonly", required=False, kind=bool, default=False)
|
||||||
+10
-2
@@ -1,6 +1,14 @@
|
|||||||
from snek.system.model import BaseModel, ModelField
|
from snek.system.model import BaseModel, ModelField
|
||||||
|
|
||||||
|
|
||||||
class DriveModel(BaseModel):
|
class DriveModel(BaseModel):
|
||||||
user_uid=ModelField(name='user_uid',required=True);name=ModelField(name='name',required=False,type=str)
|
|
||||||
|
user_uid = ModelField(name="user_uid", required=True)
|
||||||
|
name = ModelField(name="name", required=False, type=str)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
async def items(self):
|
async def items(self):
|
||||||
async for A in self.app.services.drive_item.find(drive_uid=self['uid']):yield A
|
async for drive_item in self.app.services.drive_item.find(
|
||||||
|
drive_uid=self["uid"]
|
||||||
|
):
|
||||||
|
yield drive_item
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
_B='name'
|
|
||||||
_A=True
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
|
||||||
from snek.system.model import BaseModel, ModelField
|
from snek.system.model import BaseModel, ModelField
|
||||||
|
|
||||||
|
|
||||||
class DriveItemModel(BaseModel):
|
class DriveItemModel(BaseModel):
|
||||||
drive_uid=ModelField(name='drive_uid',required=_A,kind=str);name=ModelField(name=_B,required=_A,kind=str);path=ModelField(name='path',required=_A,kind=str);file_type=ModelField(name='file_type',required=_A,kind=str);file_size=ModelField(name='file_size',required=_A,kind=int);is_available=ModelField(name='is_available',required=_A,kind=bool,initial_value=_A)
|
drive_uid = ModelField(name="drive_uid", required=True, kind=str)
|
||||||
|
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
|
@property
|
||||||
def extension(self):return self[_B].split('.')[-1]
|
def extension(self):
|
||||||
|
return self["name"].split(".")[-1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mime_type(self):A,B=mimetypes.guess_type(self[_B]);return A
|
def mime_type(self):
|
||||||
|
mimetype, _ = mimetypes.guess_type(self["name"])
|
||||||
|
return mimetype
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
_A=True
|
|
||||||
from snek.system.model import BaseModel, ModelField
|
from snek.system.model import BaseModel, ModelField
|
||||||
class NotificationModel(BaseModel):object_uid=ModelField(name='object_uid',required=_A);object_type=ModelField(name='object_type',required=_A);message=ModelField(name='message',required=_A);user_uid=ModelField(name='user_uid',required=_A);read_at=ModelField(name='is_read',required=_A)
|
|
||||||
|
|
||||||
|
class NotificationModel(BaseModel):
|
||||||
|
object_uid = ModelField(name="object_uid", required=True)
|
||||||
|
object_type = ModelField(name="object_type", required=True)
|
||||||
|
message = ModelField(name="message", required=True)
|
||||||
|
user_uid = ModelField(name="user_uid", required=True)
|
||||||
|
read_at = ModelField(name="is_read", required=True)
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
from snek.model.user import UserModel
|
from snek.model.user import UserModel
|
||||||
from snek.system.model import BaseModel, ModelField
|
from snek.system.model import BaseModel, ModelField
|
||||||
class RepositoryModel(BaseModel):user_uid=ModelField(name='user_uid',required=True,kind=str);name=ModelField(name='name',required=True,kind=str);is_private=ModelField(name='is_private',required=False,kind=bool)
|
|
||||||
|
|
||||||
|
class RepositoryModel(BaseModel):
|
||||||
|
|
||||||
|
user_uid = ModelField(name="user_uid", required=True, kind=str)
|
||||||
|
|
||||||
|
name = ModelField(name="name", required=True, kind=str)
|
||||||
|
|
||||||
|
is_private = ModelField(name="is_private", required=False, kind=bool)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+58
-15
@@ -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):
|
class UserModel(BaseModel):
|
||||||
username=ModelField(name='username',required=_B,min_length=2,max_length=20,regex=_D);nick=ModelField(name='nick',required=_B,min_length=2,max_length=20,regex=_D);color=ModelField(name='color',required=_B,regex='^#[0-9a-fA-F]{6}$',kind=str);email=ModelField(name='email',required=_C,regex='^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$');password=ModelField(name='password',required=_B,min_length=1);last_ping=ModelField(name='last_ping',required=_C,kind=str);is_admin=ModelField(name='is_admin',required=_C,kind=bool)
|
|
||||||
async def get_property(A,name):
|
username = ModelField(
|
||||||
B=await A.app.services.user_property.find_one(user_uid=A[_A],name=name)
|
name="username",
|
||||||
if B:return B['value']
|
required=True,
|
||||||
async def has_property(A,name):return await A.app.services.user_property.exists(user_uid=A[_A],name=name)
|
min_length=2,
|
||||||
async def set_property(A,name,value):
|
max_length=20,
|
||||||
C=value;B=name
|
regex=r"^[a-zA-Z0-9_-+/]+$",
|
||||||
if not await A.has_property(B):await A.app.services.user_property.insert(user_uid=A[_A],name=B,value=C)
|
)
|
||||||
else:await A.app.services.user_property.update(user_uid=A[_A],name=B,value=C)
|
nick = ModelField(
|
||||||
async def get_channel_members(A):
|
name="nick",
|
||||||
async for B in A.app.services.channel_member.find(user_uid=A[_A],is_banned=_C,deleted_at=None):yield B
|
required=True,
|
||||||
|
min_length=2,
|
||||||
|
max_length=20,
|
||||||
|
regex=r"^[a-zA-Z0-9_-+/]+$",
|
||||||
|
)
|
||||||
|
color = ModelField(
|
||||||
|
name="color", required=True, regex=r"^#[0-9a-fA-F]{6}$", kind=str
|
||||||
|
)
|
||||||
|
email = ModelField(
|
||||||
|
name="email",
|
||||||
|
required=False,
|
||||||
|
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
|
||||||
|
)
|
||||||
|
password = ModelField(name="password", required=True, min_length=1)
|
||||||
|
|
||||||
|
last_ping = ModelField(name="last_ping", required=False, kind=str)
|
||||||
|
|
||||||
|
is_admin = ModelField(name="is_admin", required=False, kind=bool)
|
||||||
|
|
||||||
|
async def get_property(self, name):
|
||||||
|
prop = await self.app.services.user_property.find_one(
|
||||||
|
user_uid=self["uid"], name=name
|
||||||
|
)
|
||||||
|
if prop:
|
||||||
|
return prop["value"]
|
||||||
|
|
||||||
|
async def has_property(self, name):
|
||||||
|
return await self.app.services.user_property.exists(
|
||||||
|
user_uid=self["uid"], name=name
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_property(self, name, value):
|
||||||
|
if not await self.has_property(name):
|
||||||
|
await self.app.services.user_property.insert(
|
||||||
|
user_uid=self["uid"], name=name, value=value
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self.app.services.user_property.update(
|
||||||
|
user_uid=self["uid"], name=name, value=value
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_channel_members(self):
|
||||||
|
async for channel_member in self.app.services.channel_member.find(
|
||||||
|
user_uid=self["uid"], is_banned=False, deleted_at=None
|
||||||
|
):
|
||||||
|
yield channel_member
|
||||||
|
|||||||
@@ -1,2 +1,7 @@
|
|||||||
from snek.system.model import BaseModel, ModelField
|
from snek.system.model import BaseModel, ModelField
|
||||||
class UserPropertyModel(BaseModel):user_uid=ModelField(name='user_uid',required=True,kind=str);name=ModelField(name='name',required=True,kind=str);value=ModelField(name='path',required=True,kind=str)
|
|
||||||
|
|
||||||
|
class UserPropertyModel(BaseModel):
|
||||||
|
user_uid = ModelField(name="user_uid", required=True, kind=str)
|
||||||
|
name = ModelField(name="name", required=True, kind=str)
|
||||||
|
value = ModelField(name="path", required=True, kind=str)
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
from aiohttp import web
|
||||||
|
import dataset
|
||||||
|
import dataset.util
|
||||||
|
import traceback
|
||||||
|
import socket
|
||||||
|
import base64
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
class DatasetMethod:
|
||||||
|
def __init__(self, dt, name):
|
||||||
|
self.dt = dt
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
return self.dt.ds.call(
|
||||||
|
self.dt.name,
|
||||||
|
self.name,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetTable:
|
||||||
|
|
||||||
|
def __init__(self, ds, name):
|
||||||
|
self.ds = ds
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return DatasetMethod(self, name)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketClient2:
|
||||||
|
def __init__(self, uri):
|
||||||
|
self.uri = uri
|
||||||
|
self.loop = asyncio.get_event_loop()
|
||||||
|
self.websocket = None
|
||||||
|
self.receive_queue = asyncio.Queue()
|
||||||
|
|
||||||
|
# Schedule connection setup
|
||||||
|
if self.loop.is_running():
|
||||||
|
# Schedule connect in the existing loop
|
||||||
|
self._connect_future = asyncio.run_coroutine_threadsafe(self._connect(), self.loop)
|
||||||
|
else:
|
||||||
|
# If loop isn't running, connect synchronously
|
||||||
|
self.loop.run_until_complete(self._connect())
|
||||||
|
|
||||||
|
async def _connect(self):
|
||||||
|
self.websocket = await websockets.connect(self.uri)
|
||||||
|
# Start listening for messages
|
||||||
|
asyncio.create_task(self._receive_loop())
|
||||||
|
|
||||||
|
async def _receive_loop(self):
|
||||||
|
try:
|
||||||
|
async for message in self.websocket:
|
||||||
|
await self.receive_queue.put(message)
|
||||||
|
except Exception:
|
||||||
|
pass # Handle exceptions as needed
|
||||||
|
|
||||||
|
def send(self, message: str):
|
||||||
|
if self.loop.is_running():
|
||||||
|
# Schedule send in the existing loop
|
||||||
|
asyncio.run_coroutine_threadsafe(self.websocket.send(message), self.loop)
|
||||||
|
else:
|
||||||
|
# If loop isn't running, run directly
|
||||||
|
self.loop.run_until_complete(self.websocket.send(message))
|
||||||
|
|
||||||
|
def receive(self):
|
||||||
|
# Wait for a message synchronously
|
||||||
|
future = asyncio.run_coroutine_threadsafe(self.receive_queue.get(), self.loop)
|
||||||
|
return future.result()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.websocket:
|
||||||
|
if self.loop.is_running():
|
||||||
|
asyncio.run_coroutine_threadsafe(self.websocket.close(), self.loop)
|
||||||
|
else:
|
||||||
|
self.loop.run_until_complete(self.websocket.close())
|
||||||
|
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
class DatasetWrapper(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.ws = WebSocketClient()
|
||||||
|
|
||||||
|
def begin(self):
|
||||||
|
self.call(None, 'begin')
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
self.call(None, 'commit')
|
||||||
|
|
||||||
|
def __getitem__(self, name):
|
||||||
|
return DatasetTable(self, name)
|
||||||
|
|
||||||
|
def query(self, *args, **kwargs):
|
||||||
|
return self.call(None, 'query', *args, **kwargs)
|
||||||
|
|
||||||
|
def call(self, table, method, *args, **kwargs):
|
||||||
|
payload = {"table": table, "method": method, "args": args, "kwargs": kwargs,"call_uid":None}
|
||||||
|
#if method in ['find','find_one']:
|
||||||
|
payload["call_uid"] = str(uuid.uuid4())
|
||||||
|
self.ws.write(json.dumps(payload))
|
||||||
|
if payload["call_uid"]:
|
||||||
|
response = self.ws.read()
|
||||||
|
return json.loads(response)['result']
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetWebSocketView:
|
||||||
|
def __init__(self):
|
||||||
|
self.ws = None
|
||||||
|
self.db = dataset.connect('sqlite:///snek.db')
|
||||||
|
self.setattr(self, "db", self.get)
|
||||||
|
self.setattr(self, "db", self.set)
|
||||||
|
)
|
||||||
|
super()
|
||||||
|
|
||||||
|
def format_result(self, result):
|
||||||
|
|
||||||
|
try:
|
||||||
|
return dict(result)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return [dict(row) for row in result]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def send_str(self, msg):
|
||||||
|
return await self.ws.send_str(msg)
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
returnl loads(dict(self.db['_kv'].get(key=key)['value']))
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
return self.db['_kv'].upsert({'key': key, 'value': json.dumps(value)}, ['key'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def handle(self, request):
|
||||||
|
ws = web.WebSocketResponse()
|
||||||
|
await ws.prepare(request)
|
||||||
|
self.ws = ws
|
||||||
|
|
||||||
|
async for msg in ws:
|
||||||
|
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||||
|
try:
|
||||||
|
data = json.loads(msg.data)
|
||||||
|
call_uid = data.get("call_uid")
|
||||||
|
method = data.get("method")
|
||||||
|
table_name = data.get("table")
|
||||||
|
args = data.get("args", {})
|
||||||
|
kwargs = data.get("kwargs", {})
|
||||||
|
|
||||||
|
|
||||||
|
function = getattr(self.db, method, None)
|
||||||
|
if table_name:
|
||||||
|
function = getattr(self.db[table_name], method, None)
|
||||||
|
|
||||||
|
print(method, table_name, args, kwargs,flush=True)
|
||||||
|
|
||||||
|
if function:
|
||||||
|
response = {}
|
||||||
|
try:
|
||||||
|
result = function(*args, **kwargs)
|
||||||
|
print(result)
|
||||||
|
response['result'] = self.format_result(result)
|
||||||
|
response["call_uid"] = call_uid
|
||||||
|
response["success"] = True
|
||||||
|
except Exception as e:
|
||||||
|
response["call_uid"] = call_uid
|
||||||
|
response["success"] = False
|
||||||
|
response["error"] = str(e)
|
||||||
|
response["traceback"] = traceback.format_exc()
|
||||||
|
|
||||||
|
if call_uid:
|
||||||
|
await self.send_str(json.dumps(response,default=str))
|
||||||
|
else:
|
||||||
|
await self.send_str(json.dumps({"status": "error", "error":"Method not found.","call_uid": call_uid}))
|
||||||
|
except Exception as e:
|
||||||
|
await self.send_str(json.dumps({"success": False,"call_uid": call_uid, "error": str(e), "error": str(e), "traceback": traceback.format_exc()},default=str))
|
||||||
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
|
print('ws connection closed with exception %s' % ws.exception())
|
||||||
|
|
||||||
|
return ws
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
view = DatasetWebSocketView()
|
||||||
|
app.router.add_get('/db', view.handle)
|
||||||
|
|
||||||
|
async def run_server():
|
||||||
|
|
||||||
|
|
||||||
|
runner = web.AppRunner(app)
|
||||||
|
await runner.setup()
|
||||||
|
site = web.TCPSite(runner, 'localhost', 3131)
|
||||||
|
await site.start()
|
||||||
|
|
||||||
|
print("Server started at http://localhost:8080")
|
||||||
|
await asyncio.Event().wait()
|
||||||
|
|
||||||
|
async def client():
|
||||||
|
print("x")
|
||||||
|
d = DatasetWrapper()
|
||||||
|
print("y")
|
||||||
|
|
||||||
|
for x in range(100):
|
||||||
|
for x in range(100):
|
||||||
|
if d['test'].insert({"name": "test", "number":x}):
|
||||||
|
print(".",end="",flush=True)
|
||||||
|
print("")
|
||||||
|
print(d['test'].find_one(name="test", order_by="-number"))
|
||||||
|
|
||||||
|
print("DONE")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import time
|
||||||
|
async def main():
|
||||||
|
await run_server()
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if sys.argv[1] == 'server':
|
||||||
|
asyncio.run(main())
|
||||||
|
if sys.argv[1] == 'client':
|
||||||
|
asyncio.run(client())
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import snek.serpentarium
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
|
|
||||||
|
durations = []
|
||||||
|
|
||||||
|
def task1():
|
||||||
|
global durations
|
||||||
|
client = snek.serpentarium.DatasetWrapper()
|
||||||
|
|
||||||
|
start=time.time()
|
||||||
|
for x in range(1500):
|
||||||
|
|
||||||
|
client['a'].delete()
|
||||||
|
client['a'].insert({"foo": x})
|
||||||
|
client['a'].find(foo=x)
|
||||||
|
client['a'].find_one(foo=x)
|
||||||
|
client['a'].count()
|
||||||
|
#print(client['a'].find(foo=x) )
|
||||||
|
#print(client['a'].find_one(foo=x) )
|
||||||
|
#print(client['a'].count())
|
||||||
|
client.close()
|
||||||
|
duration1 = f"{time.time()-start}"
|
||||||
|
durations.append(duration1)
|
||||||
|
print(durations)
|
||||||
|
|
||||||
|
with ProcessPoolExecutor(max_workers=4) as executor:
|
||||||
|
tasks = [executor.submit(task1),
|
||||||
|
executor.submit(task1),
|
||||||
|
executor.submit(task1),
|
||||||
|
executor.submit(task1)
|
||||||
|
]
|
||||||
|
for task in tasks:
|
||||||
|
task.result()
|
||||||
|
|
||||||
|
|
||||||
|
import dataset
|
||||||
|
client = dataset.connect("sqlite:///snek.db")
|
||||||
|
start=time.time()
|
||||||
|
for x in range(1500):
|
||||||
|
|
||||||
|
client['a'].delete()
|
||||||
|
client['a'].insert({"foo": x})
|
||||||
|
print([dict(row) for row in client['a'].find(foo=x)])
|
||||||
|
print(dict(client['a'].find_one(foo=x) ))
|
||||||
|
print(client['a'].count())
|
||||||
|
duration2 = f"{time.time()-start}"
|
||||||
|
|
||||||
|
print(duration1,duration2)
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS http_access (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
created TEXT,
|
||||||
|
path TEXT,
|
||||||
|
duration FLOAT,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS user (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
color TEXT,
|
||||||
|
created_at TEXT,
|
||||||
|
deleted_at TEXT,
|
||||||
|
email TEXT,
|
||||||
|
is_admin TEXT,
|
||||||
|
last_ping TEXT,
|
||||||
|
nick TEXT,
|
||||||
|
password TEXT,
|
||||||
|
uid TEXT,
|
||||||
|
updated_at TEXT,
|
||||||
|
username TEXT,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_user_e2577dd78b54fe28 ON user (uid);
|
||||||
|
CREATE TABLE IF NOT EXISTS channel (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
created_at TEXT,
|
||||||
|
created_by_uid TEXT,
|
||||||
|
deleted_at TEXT,
|
||||||
|
description TEXT,
|
||||||
|
"index" BIGINT,
|
||||||
|
is_listed BOOLEAN,
|
||||||
|
is_private BOOLEAN,
|
||||||
|
label TEXT,
|
||||||
|
last_message_on TEXT,
|
||||||
|
tag TEXT,
|
||||||
|
uid TEXT,
|
||||||
|
updated_at TEXT,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_channel_e2577dd78b54fe28 ON channel (uid);
|
||||||
|
CREATE TABLE IF NOT EXISTS channel_member (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
channel_uid TEXT,
|
||||||
|
created_at TEXT,
|
||||||
|
deleted_at TEXT,
|
||||||
|
is_banned BOOLEAN,
|
||||||
|
is_moderator BOOLEAN,
|
||||||
|
is_muted BOOLEAN,
|
||||||
|
is_read_only BOOLEAN,
|
||||||
|
label TEXT,
|
||||||
|
new_count BIGINT,
|
||||||
|
uid TEXT,
|
||||||
|
updated_at TEXT,
|
||||||
|
user_uid TEXT,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_channel_member_e2577dd78b54fe28 ON channel_member (uid);
|
||||||
|
CREATE TABLE IF NOT EXISTS broadcast (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
channel_uid TEXT,
|
||||||
|
message TEXT,
|
||||||
|
created_at TEXT,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS channel_message (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
channel_uid TEXT,
|
||||||
|
created_at TEXT,
|
||||||
|
deleted_at TEXT,
|
||||||
|
html TEXT,
|
||||||
|
message TEXT,
|
||||||
|
uid TEXT,
|
||||||
|
updated_at TEXT,
|
||||||
|
user_uid TEXT,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_channel_message_e2577dd78b54fe28 ON channel_message (uid);
|
||||||
|
CREATE TABLE IF NOT EXISTS notification (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
created_at TEXT,
|
||||||
|
deleted_at TEXT,
|
||||||
|
message TEXT,
|
||||||
|
object_type TEXT,
|
||||||
|
object_uid TEXT,
|
||||||
|
read_at TEXT,
|
||||||
|
uid TEXT,
|
||||||
|
updated_at TEXT,
|
||||||
|
user_uid TEXT,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_notification_e2577dd78b54fe28 ON notification (uid);
|
||||||
|
CREATE TABLE IF NOT EXISTS repository (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
created_at TEXT,
|
||||||
|
deleted_at TEXT,
|
||||||
|
is_private BIGINT,
|
||||||
|
name TEXT,
|
||||||
|
uid TEXT,
|
||||||
|
updated_at TEXT,
|
||||||
|
user_uid TEXT,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_repository_e2577dd78b54fe28 ON repository (uid);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import functools
|
import functools
|
||||||
|
|
||||||
from snek.service.channel import ChannelService
|
from snek.service.channel import ChannelService
|
||||||
from snek.service.channel_member import ChannelMemberService
|
from snek.service.channel_member import ChannelMemberService
|
||||||
from snek.service.channel_message import ChannelMessageService
|
from snek.service.channel_message import ChannelMessageService
|
||||||
@@ -11,7 +12,33 @@ from snek.service.user import UserService
|
|||||||
from snek.service.user_property import UserPropertyService
|
from snek.service.user_property import UserPropertyService
|
||||||
from snek.service.util import UtilService
|
from snek.service.util import UtilService
|
||||||
from snek.service.repository import RepositoryService
|
from snek.service.repository import RepositoryService
|
||||||
|
from snek.service.channel_attachment import ChannelAttachmentService
|
||||||
|
from snek.service.container import ContainerService
|
||||||
from snek.system.object import Object
|
from snek.system.object import Object
|
||||||
|
from snek.service.db import DBService
|
||||||
|
|
||||||
@functools.cache
|
@functools.cache
|
||||||
def get_services(app):A=app;return Object(**{'user':UserService(app=A),'channel_member':ChannelMemberService(app=A),'channel':ChannelService(app=A),'channel_message':ChannelMessageService(app=A),'chat':ChatService(app=A),'socket':SocketService(app=A),'notification':NotificationService(app=A),'util':UtilService(app=A),'drive':DriveService(app=A),'drive_item':DriveItemService(app=A),'user_property':UserPropertyService(app=A),'repository':RepositoryService(app=A)})
|
def get_services(app):
|
||||||
def get_service(name,app=None):return get_services(app=app)[name]
|
return Object(
|
||||||
|
**{
|
||||||
|
"user": UserService(app=app),
|
||||||
|
"channel_member": ChannelMemberService(app=app),
|
||||||
|
"channel": ChannelService(app=app),
|
||||||
|
"channel_message": ChannelMessageService(app=app),
|
||||||
|
"chat": ChatService(app=app),
|
||||||
|
"socket": SocketService(app=app),
|
||||||
|
"notification": NotificationService(app=app),
|
||||||
|
"util": UtilService(app=app),
|
||||||
|
"drive": DriveService(app=app),
|
||||||
|
"drive_item": DriveItemService(app=app),
|
||||||
|
"user_property": UserPropertyService(app=app),
|
||||||
|
"repository": RepositoryService(app=app),
|
||||||
|
"db": DBService(app=app),
|
||||||
|
"channel_attachment": ChannelAttachmentService(app=app),
|
||||||
|
"container": ContainerService(app=app),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_service(name, app=None):
|
||||||
|
return get_services(app=app)[name]
|
||||||
|
|||||||
+117
-45
@@ -1,49 +1,121 @@
|
|||||||
_F='channel_uid'
|
|
||||||
_E='public'
|
|
||||||
_D=True
|
|
||||||
_C='uid'
|
|
||||||
_B=None
|
|
||||||
_A=False
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from snek.system.model import now
|
from snek.system.model import now
|
||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
|
||||||
class ChannelService(BaseService):
|
class ChannelService(BaseService):
|
||||||
mapper_name='channel'
|
mapper_name = "channel"
|
||||||
async def get(E,uid=_B,**A):
|
|
||||||
D='name';C=uid
|
async def get_attachment_folder(self, channel_uid,ensure=False):
|
||||||
if C:
|
path = pathlib.Path(f"./drive/{channel_uid}/attachments")
|
||||||
A[_C]=C;B=await super().get(**A)
|
if ensure:
|
||||||
if B:return B
|
path.mkdir(
|
||||||
del A[_C];A[D]=C;B=await super().get(**A)
|
parents=True, exist_ok=True
|
||||||
if B:return B
|
)
|
||||||
A[D]='#'+C;B=await super().get(**A)
|
return path
|
||||||
if B:return B
|
|
||||||
return
|
async def get(self, uid=None, **kwargs):
|
||||||
return await super().get(**A)
|
if uid:
|
||||||
async def create(C,label,created_by_uid,description=_B,tag=_B,is_private=_A,is_listed=_D):
|
kwargs["uid"] = uid
|
||||||
E=is_listed;D=tag;B=label
|
result = await super().get(**kwargs)
|
||||||
if B[0]!='#'and E:B=f"#{B}"
|
if result:
|
||||||
F=await C.count(deleted_at=_B)
|
return result
|
||||||
if not D and not F:D=_E
|
del kwargs["uid"]
|
||||||
A=await C.new();A['label']=B;A['description']=description;A['tag']=D;A['created_by_uid']=created_by_uid;A['is_private']=is_private;A['is_listed']=E
|
kwargs["name"] = uid
|
||||||
if await C.save(A):return A
|
result = await super().get(**kwargs)
|
||||||
raise Exception(f"Failed to create channel: {A.errors}.")
|
if result:
|
||||||
async def get_dm(A,user1,user2):
|
return result
|
||||||
C=user2;B=user1;D=await A.services.channel_member.get_dm(B,C)
|
kwargs["name"] = "#" + uid
|
||||||
if D:return await A.get(uid=D[_F])
|
result = await super().get(**kwargs)
|
||||||
E=await A.create('DM',B,tag='dm');await A.services.channel_member.create_dm(E[_C],B,C);return E
|
if result:
|
||||||
async def get_users(A,channel_uid):
|
return result
|
||||||
async for C in A.services.channel_member.find(channel_uid=channel_uid,is_banned=_A,is_muted=_A,deleted_at=_B):
|
return None
|
||||||
B=await A.services.user.get(uid=C['user_uid'])
|
return await super().get(**kwargs)
|
||||||
if B:yield B
|
|
||||||
async def get_online_users(C,channel_uid):
|
async def create(
|
||||||
B='last_ping'
|
self,
|
||||||
async for A in C.get_users(channel_uid):
|
label,
|
||||||
if not A[B]:continue
|
created_by_uid,
|
||||||
if(datetime.fromisoformat(now())-datetime.fromisoformat(A[B])).total_seconds()<20:yield A
|
description=None,
|
||||||
async def get_for_user(A,user_uid):
|
tag=None,
|
||||||
async for B in A.services.channel_member.find(user_uid=user_uid,is_banned=_A,deleted_at=_B):C=await A.get(uid=B[_F]);yield C
|
is_private=False,
|
||||||
async def ensure_public_channel(B,created_by_uid):
|
is_listed=True,
|
||||||
C=created_by_uid;A=await B.get(is_listed=_D,tag=_E);D=_A
|
):
|
||||||
if not A:D=_D;A=await B.create(_E,created_by_uid=C,is_listed=_D,tag=_E)
|
if label[0] != "#" and is_listed:
|
||||||
await B.app.services.channel_member.create(A[_C],C,is_moderator=D,is_read_only=_A,is_muted=_A,is_banned=_A);return A
|
label = f"#{label}"
|
||||||
|
count = await self.count(deleted_at=None)
|
||||||
|
if not tag and not count:
|
||||||
|
tag = "public"
|
||||||
|
model = await self.new()
|
||||||
|
model["label"] = label
|
||||||
|
model["description"] = description
|
||||||
|
model["tag"] = tag
|
||||||
|
model["created_by_uid"] = created_by_uid
|
||||||
|
model["is_private"] = is_private
|
||||||
|
model["is_listed"] = is_listed
|
||||||
|
if await self.save(model):
|
||||||
|
return model
|
||||||
|
raise Exception(f"Failed to create channel: {model.errors}.")
|
||||||
|
|
||||||
|
async def get_dm(self, user1, user2):
|
||||||
|
channel_member = await self.services.channel_member.get_dm(user1, user2)
|
||||||
|
if channel_member:
|
||||||
|
return await self.get(uid=channel_member["channel_uid"])
|
||||||
|
channel = await self.create("DM", user1, tag="dm")
|
||||||
|
await self.services.channel_member.create_dm(channel["uid"], user1, user2)
|
||||||
|
return channel
|
||||||
|
|
||||||
|
async def get_recent_users(self, channel_uid):
|
||||||
|
async for user in self.query("SELECT user.uid, user.username,user.color,user.last_ping,user.nick FROM channel_member INNER JOIN user ON user.uid = channel_member.user_uid WHERE channel_uid=:channel_uid AND user.last_ping >= datetime('now', '-3 minutes') ORDER BY last_ping DESC LIMIT 30", {"channel_uid": channel_uid}):
|
||||||
|
yield user
|
||||||
|
|
||||||
|
async def get_users(self, channel_uid):
|
||||||
|
async for channel_member in self.services.channel_member.find(
|
||||||
|
channel_uid=channel_uid,
|
||||||
|
is_banned=False,
|
||||||
|
is_muted=False,
|
||||||
|
deleted_at=None,
|
||||||
|
):
|
||||||
|
user = await self.services.user.get(uid=channel_member["user_uid"])
|
||||||
|
if user:
|
||||||
|
yield user
|
||||||
|
|
||||||
|
async def get_online_users(self, channel_uid):
|
||||||
|
async for user in self.get_users(channel_uid):
|
||||||
|
if not user["last_ping"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
datetime.fromisoformat(now())
|
||||||
|
- datetime.fromisoformat(user["last_ping"])
|
||||||
|
).total_seconds() < 180:
|
||||||
|
yield user
|
||||||
|
|
||||||
|
async def get_for_user(self, user_uid):
|
||||||
|
async for channel_member in self.services.channel_member.find(
|
||||||
|
user_uid=user_uid,
|
||||||
|
is_banned=False,
|
||||||
|
deleted_at=None,
|
||||||
|
):
|
||||||
|
channel = await self.get(uid=channel_member["channel_uid"])
|
||||||
|
yield channel
|
||||||
|
|
||||||
|
async def ensure_public_channel(self, created_by_uid):
|
||||||
|
model = await self.get(is_listed=True, tag="public")
|
||||||
|
is_moderator = False
|
||||||
|
if not model:
|
||||||
|
is_moderator = True
|
||||||
|
model = await self.create(
|
||||||
|
"public", created_by_uid=created_by_uid, is_listed=True, tag="public"
|
||||||
|
)
|
||||||
|
await self.app.services.channel_member.create(
|
||||||
|
model["uid"],
|
||||||
|
created_by_uid,
|
||||||
|
is_moderator=is_moderator,
|
||||||
|
is_read_only=False,
|
||||||
|
is_muted=False,
|
||||||
|
is_banned=False,
|
||||||
|
)
|
||||||
|
return model
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from snek.system.service import BaseService
|
||||||
|
import urllib.parse
|
||||||
|
import pathlib
|
||||||
|
import mimetypes
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
class ChannelAttachmentService(BaseService):
|
||||||
|
mapper_name="channel_attachment"
|
||||||
|
|
||||||
|
async def create_file(self, channel_uid, user_uid, name):
|
||||||
|
attachment = await self.new()
|
||||||
|
attachment["channel_uid"] = channel_uid
|
||||||
|
attachment['user_uid'] = user_uid
|
||||||
|
attachment["name"] = name
|
||||||
|
attachment["mime_type"] = mimetypes.guess_type(name)[0]
|
||||||
|
attachment['resource_type'] = "file"
|
||||||
|
real_file_name = f"{attachment['uid']}-{name}"
|
||||||
|
attachment["relative_url"] = (f"{attachment['uid']}-{name}")
|
||||||
|
attachment_folder = await self.services.channel.get_attachment_folder(channel_uid)
|
||||||
|
attachment_path = attachment_folder.joinpath(real_file_name)
|
||||||
|
attachment["path"] = str(attachment_path)
|
||||||
|
if await self.save(attachment):
|
||||||
|
return attachment
|
||||||
|
raise Exception(f"Failed to create channel attachment: {attachment.errors}.")
|
||||||
|
|
||||||
@@ -1,28 +1,74 @@
|
|||||||
_C='user_uid'
|
|
||||||
_B='channel_uid'
|
|
||||||
_A=False
|
|
||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
|
||||||
class ChannelMemberService(BaseService):
|
class ChannelMemberService(BaseService):
|
||||||
mapper_name='channel_member'
|
|
||||||
async def mark_as_read(A,channel_uid,user_uid):B=await A.get(channel_uid=channel_uid,user_uid=user_uid);B['new_count']=0;return await A.save(B)
|
mapper_name = "channel_member"
|
||||||
async def get_user_uids(A,channel_uid):
|
|
||||||
async for B in A.mapper.query('SELECT user_uid FROM channel_member WHERE channel_uid=:channel_uid',{_B:channel_uid}):yield B[_C]
|
async def mark_as_read(self, channel_uid, user_uid):
|
||||||
async def create(B,channel_uid,user_uid,is_moderator=_A,is_read_only=_A,is_muted=_A,is_banned=_A):
|
channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)
|
||||||
D='label';E='is_banned';F=user_uid;C=channel_uid;A=await B.get(channel_uid=C,user_uid=F)
|
channel_member["new_count"] = 0
|
||||||
if A:
|
return await self.save(channel_member)
|
||||||
if A[E]:return _A
|
|
||||||
return A
|
async def get_user_uids(self, channel_uid):
|
||||||
A=await B.new();G=await B.services.channel.get(uid=C);A[D]=G[D];A[_B]=C;A[_C]=F;A['is_moderator']=is_moderator;A['is_read_only']=is_read_only;A['is_muted']=is_muted;A[E]=is_banned
|
async for model in self.mapper.query(
|
||||||
if await B.save(A):return A
|
"SELECT user_uid FROM channel_member WHERE channel_uid=:channel_uid",
|
||||||
raise Exception(f"Failed to create channel member: {A.errors}.")
|
{"channel_uid": channel_uid},
|
||||||
async def get_dm(D,from_user,to_user):
|
):
|
||||||
E='to_user';F='from_user';A=to_user;B=from_user
|
yield model["user_uid"]
|
||||||
async for C in D.query("SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') INNER JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = channel.uid AND channel_member2.user_uid = :to_user) WHERE channel_member.user_uid=:from_user ",{F:B,E:A}):return C
|
|
||||||
if not B==A:return
|
async def create(
|
||||||
async for C in D.query("SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') LEFT JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = NULL AND channel_member2.user_uid = NULL) WHERE channel_member.user_uid=:from_user ",{F:B,E:A}):return C
|
self,
|
||||||
async def get_other_dm_user(A,channel_uid,user_uid):
|
channel_uid,
|
||||||
B='uid';C=channel_uid;D=await A.get(channel_uid=C,user_uid=user_uid);F=await A.services.channel.get(uid=D[_B])
|
user_uid,
|
||||||
if F['tag']!='dm':return
|
is_moderator=False,
|
||||||
async for E in A.services.channel_member.find(channel_uid=C):
|
is_read_only=False,
|
||||||
if E[B]!=D[B]:return await A.services.user.get(uid=E[_C])
|
is_muted=False,
|
||||||
async def create_dm(A,channel_uid,from_user_uid,to_user_uid):B=channel_uid;C=await A.create(B,from_user_uid);await A.create(B,to_user_uid);return C
|
is_banned=False,
|
||||||
|
):
|
||||||
|
model = await self.get(channel_uid=channel_uid, user_uid=user_uid)
|
||||||
|
if model:
|
||||||
|
if model["is_banned"]:
|
||||||
|
return False
|
||||||
|
return model
|
||||||
|
model = await self.new()
|
||||||
|
channel = await self.services.channel.get(uid=channel_uid)
|
||||||
|
model["label"] = channel["label"]
|
||||||
|
model["channel_uid"] = channel_uid
|
||||||
|
model["user_uid"] = user_uid
|
||||||
|
model["is_moderator"] = is_moderator
|
||||||
|
model["is_read_only"] = is_read_only
|
||||||
|
model["is_muted"] = is_muted
|
||||||
|
model["is_banned"] = is_banned
|
||||||
|
if await self.save(model):
|
||||||
|
return model
|
||||||
|
raise Exception(f"Failed to create channel member: {model.errors}.")
|
||||||
|
|
||||||
|
async def get_dm(self, from_user, to_user):
|
||||||
|
async for model in self.query(
|
||||||
|
"SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') INNER JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = channel.uid AND channel_member2.user_uid = :to_user) WHERE channel_member.user_uid=:from_user ",
|
||||||
|
{"from_user": from_user, "to_user": to_user},
|
||||||
|
):
|
||||||
|
return model
|
||||||
|
if not from_user == to_user:
|
||||||
|
return None
|
||||||
|
async for model in self.query(
|
||||||
|
"SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') LEFT JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = NULL AND channel_member2.user_uid = NULL) WHERE channel_member.user_uid=:from_user ",
|
||||||
|
{"from_user": from_user, "to_user": to_user},
|
||||||
|
):
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
async def get_other_dm_user(self, channel_uid, user_uid):
|
||||||
|
channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)
|
||||||
|
channel = await self.services.channel.get(uid=channel_member["channel_uid"])
|
||||||
|
if channel["tag"] != "dm":
|
||||||
|
return None
|
||||||
|
async for model in self.services.channel_member.find(channel_uid=channel_uid):
|
||||||
|
if model["uid"] != channel_member["uid"]:
|
||||||
|
return await self.services.user.get(uid=model["user_uid"])
|
||||||
|
|
||||||
|
async def create_dm(self, channel_uid, from_user_uid, to_user_uid):
|
||||||
|
result = await self.create(channel_uid, from_user_uid)
|
||||||
|
await self.create(channel_uid, to_user_uid)
|
||||||
|
return result
|
||||||
|
|||||||
@@ -1,33 +1,110 @@
|
|||||||
_I='user_nick'
|
|
||||||
_H='created_at'
|
|
||||||
_G='html'
|
|
||||||
_F='uid'
|
|
||||||
_E='message'
|
|
||||||
_D='color'
|
|
||||||
_C='username'
|
|
||||||
_B='user_uid'
|
|
||||||
_A='channel_uid'
|
|
||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
|
||||||
class ChannelMessageService(BaseService):
|
class ChannelMessageService(BaseService):
|
||||||
mapper_name='channel_message'
|
mapper_name = "channel_message"
|
||||||
async def create(B,channel_uid,user_uid,message):
|
|
||||||
E=user_uid;A=await B.new();A[_A]=channel_uid;A[_B]=E;A[_E]=message;D={};F=A.record;D.update(F);C=await B.app.services.user.get(uid=E);D.update({_B:C[_F],_C:C[_C],_I:C['nick'],_D:C[_D]})
|
async def create(self, channel_uid, user_uid, message, is_final=True):
|
||||||
try:G=B.app.jinja2_env.get_template('message.html');A[_G]=G.render(**D)
|
model = await self.new()
|
||||||
except Exception as H:print(H,flush=True)
|
|
||||||
if await B.save(A):return A
|
model["channel_uid"] = channel_uid
|
||||||
raise Exception(f"Failed to create channel message: {A.errors}.")
|
model["user_uid"] = user_uid
|
||||||
async def to_extended_dict(C,message):
|
model["message"] = message
|
||||||
A=message;B=await C.services.user.get(uid=A[_B])
|
model['is_final'] = is_final
|
||||||
if not B:return{}
|
|
||||||
return{_F:A[_F],_D:B[_D],_B:A[_B],_A:A[_A],_I:B['nick'],_E:A[_E],_H:A[_H],_G:A[_G],_C:B[_C]}
|
context = {}
|
||||||
async def offset(D,channel_uid,page=0,timestamp=None,page_size=30):
|
|
||||||
J='timestamp';E='offset';F='page_size';G=timestamp;H=channel_uid;C=page_size;A=[];I=page*C
|
record = model.record
|
||||||
|
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:
|
try:
|
||||||
if G:
|
template = self.app.jinja2_env.get_template("message.html")
|
||||||
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)
|
model["html"] = template.render(**context)
|
||||||
|
except Exception as ex:
|
||||||
|
print(ex, flush=True)
|
||||||
|
|
||||||
|
if await super().save(model):
|
||||||
|
return model
|
||||||
|
raise Exception(f"Failed to create channel message: {model.errors}.")
|
||||||
|
|
||||||
|
async def to_extended_dict(self, message):
|
||||||
|
user = await self.services.user.get(uid=message["user_uid"])
|
||||||
|
if not user:
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"uid": message["uid"],
|
||||||
|
"color": user["color"],
|
||||||
|
"user_uid": message["user_uid"],
|
||||||
|
"channel_uid": message["channel_uid"],
|
||||||
|
"user_nick": user["nick"],
|
||||||
|
"message": message["message"],
|
||||||
|
"created_at": message["created_at"],
|
||||||
|
"html": message["html"],
|
||||||
|
"username": user["username"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def save(self, model):
|
||||||
|
context = {}
|
||||||
|
context.update(model.record)
|
||||||
|
user = await self.app.services.user.get(model['user_uid'])
|
||||||
|
context.update(
|
||||||
|
{
|
||||||
|
"user_uid": user["uid"],
|
||||||
|
"username": user["username"],
|
||||||
|
"user_nick": user["nick"],
|
||||||
|
"color": user["color"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
template = self.app.jinja2_env.get_template("message.html")
|
||||||
|
model["html"] = template.render(**context)
|
||||||
|
return await super().save(model)
|
||||||
|
|
||||||
|
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
||||||
|
results = []
|
||||||
|
offset = page * page_size
|
||||||
|
try:
|
||||||
|
if timestamp:
|
||||||
|
async for model in self.query(
|
||||||
|
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid AND created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",
|
||||||
|
{
|
||||||
|
"channel_uid": channel_uid,
|
||||||
|
"page_size": page_size,
|
||||||
|
"offset": offset,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
},
|
||||||
|
):
|
||||||
|
results.append(model)
|
||||||
elif page > 0:
|
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)
|
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:
|
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)
|
async for model in self.query(
|
||||||
except:pass
|
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",
|
||||||
A.sort(key=lambda x:x[_H]);return A
|
{
|
||||||
|
"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
|
||||||
|
|||||||
@@ -1,7 +1,66 @@
|
|||||||
from snek.system.model import now
|
from snek.system.model import now
|
||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
|
||||||
class ChatService(BaseService):
|
class ChatService(BaseService):
|
||||||
async def send(A,user_uid,channel_uid,message):
|
|
||||||
H='username';I='created_at';J='color';K='html';L='message';D='uid';E=user_uid;C=channel_uid;F=await A.services.channel.get(uid=C)
|
async def finalize(self, message_uid):
|
||||||
if not F:raise Exception('Channel not found.')
|
channel_message = await self.services.channel_message.get(uid=message_uid)
|
||||||
B=await A.services.channel_message.create(C,E,message);M=B[D];G=await A.services.user.get(uid=E);F['last_message_on']=now();await A.services.channel.save(F);await A.services.socket.broadcast(C,{L:B[L],K:B[K],'user_uid':E,J:G[J],'channel_uid':C,I:B[I],'updated_at':None,H:G[H],D:B[D],'user_nick':G['nick']});await A.app.create_task(A.services.notification.create_channel_message(M));return True
|
channel_message["is_final"] = True
|
||||||
|
await self.services.channel_message.save(channel_message)
|
||||||
|
user = await self.services.user.get(uid=channel_message["user_uid"])
|
||||||
|
channel = await self.services.channel.get(uid=channel_message["channel_uid"])
|
||||||
|
channel["last_message_on"] = now()
|
||||||
|
await self.services.channel.save(channel)
|
||||||
|
await self.services.socket.broadcast(
|
||||||
|
channel['uid'],
|
||||||
|
{
|
||||||
|
"message": channel_message["message"],
|
||||||
|
"html": channel_message["html"],
|
||||||
|
"user_uid": user['uid'],
|
||||||
|
"color": user["color"],
|
||||||
|
"channel_uid": channel['uid'],
|
||||||
|
"created_at": channel_message["created_at"],
|
||||||
|
"updated_at": channel_message["updated_at"],
|
||||||
|
"username": user["username"],
|
||||||
|
"uid": channel_message["uid"],
|
||||||
|
"user_nick": user["nick"],
|
||||||
|
"is_final": channel_message["is_final"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def send(self, user_uid, channel_uid, message, is_final=True):
|
||||||
|
channel = await self.services.channel.get(uid=channel_uid)
|
||||||
|
if not channel:
|
||||||
|
raise Exception("Channel not found.")
|
||||||
|
channel_message = await self.services.channel_message.create(
|
||||||
|
channel_uid, user_uid, message,is_final
|
||||||
|
)
|
||||||
|
channel_message_uid = channel_message["uid"]
|
||||||
|
|
||||||
|
user = await self.services.user.get(uid=user_uid)
|
||||||
|
channel["last_message_on"] = now()
|
||||||
|
await self.services.channel.save(channel)
|
||||||
|
|
||||||
|
await self.services.socket.broadcast(
|
||||||
|
channel_uid,
|
||||||
|
{
|
||||||
|
"message": channel_message["message"],
|
||||||
|
"html": channel_message["html"],
|
||||||
|
"user_uid": user_uid,
|
||||||
|
"color": user["color"],
|
||||||
|
"channel_uid": channel_uid,
|
||||||
|
"created_at": channel_message["created_at"],
|
||||||
|
"updated_at": None,
|
||||||
|
"username": user["username"],
|
||||||
|
"uid": channel_message["uid"],
|
||||||
|
"user_nick": user["nick"],
|
||||||
|
"is_final": is_final,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await self.app.create_task(
|
||||||
|
self.services.notification.create_channel_message(channel_message_uid)
|
||||||
|
)
|
||||||
|
|
||||||
|
return channel_message
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
class ContainerService(BaseService):
|
||||||
|
mapper_name = "container"
|
||||||
|
|
||||||
|
async def create(self, id, name, status, resources=None, user_uid=None, path=None, readonly=False):
|
||||||
|
model = await self.new()
|
||||||
|
model["id"] = id
|
||||||
|
model["name"] = name
|
||||||
|
model["status"] = status
|
||||||
|
if resources:
|
||||||
|
model["resources"] = resources
|
||||||
|
if user_uid:
|
||||||
|
model["user_uid"] = user_uid
|
||||||
|
if path:
|
||||||
|
model["path"] = path
|
||||||
|
model["readonly"] = readonly
|
||||||
|
if await super().save(model):
|
||||||
|
return model
|
||||||
|
raise Exception(f"Failed to create container: {model.errors}")
|
||||||
|
|
||||||
|
async def get(self, id):
|
||||||
|
return await self.mapper.get(id)
|
||||||
|
|
||||||
|
async def update(self, model):
|
||||||
|
return await self.mapper.update(model)
|
||||||
|
|
||||||
|
async def delete(self, id):
|
||||||
|
return await self.mapper.delete(id)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
from snek.system.service import BaseService
|
||||||
|
import dataset
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class DBService(BaseService):
|
||||||
|
|
||||||
|
async def get_db(self, user_uid):
|
||||||
|
|
||||||
|
home_folder = await self.app.services.user.get_home_folder(user_uid)
|
||||||
|
home_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
db_path = home_folder.joinpath("snek/user.db")
|
||||||
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
return dataset.connect("sqlite:///" + str(db_path))
|
||||||
|
|
||||||
|
async def insert(self, user_uid, table_name, values):
|
||||||
|
db = await self.get_db(user_uid)
|
||||||
|
return db[table_name].insert(values)
|
||||||
|
|
||||||
|
|
||||||
|
async def update(self, user_uid, table_name, values, filters):
|
||||||
|
db = await self.get_db(user_uid)
|
||||||
|
if not filters:
|
||||||
|
filters = {}
|
||||||
|
if not values:
|
||||||
|
return False
|
||||||
|
return db[table_name].update(values, filters)
|
||||||
|
|
||||||
|
async def upsert(self, user_uid, table_name, values, keys):
|
||||||
|
db = await self.get_db(user_uid)
|
||||||
|
return db[table_name].upsert(values, keys)
|
||||||
|
|
||||||
|
async def find(self, user_uid, table_name, kwargs):
|
||||||
|
db = await self.get_db(user_uid)
|
||||||
|
kwargs['_limit'] = kwargs.get('_limit', 30)
|
||||||
|
return [dict(row) for row in db[table_name].find(**kwargs)]
|
||||||
|
|
||||||
|
async def get(self, user_uid, table_name, filters):
|
||||||
|
db = await self.get_db(user_uid)
|
||||||
|
if not filters:
|
||||||
|
filters = {}
|
||||||
|
try:
|
||||||
|
return dict(db[table_name].find_one(**filters))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def delete(self, user_uid, table_name, filters):
|
||||||
|
db = await self.get_db(user_uid)
|
||||||
|
if not filters:
|
||||||
|
filters = {}
|
||||||
|
return db[table_name].delete(**filters)
|
||||||
|
|
||||||
|
async def query(self, sql,values):
|
||||||
|
db = await self.app.db
|
||||||
|
return [dict(row) for row in db.query(sql, values or {})]
|
||||||
|
|
||||||
|
async def exists(self, user_uid, table_name, filters):
|
||||||
|
db = await self.get_db(user_uid)
|
||||||
|
if not filters:
|
||||||
|
filters = {}
|
||||||
|
return bool(db[table_name].find_one(**filters))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def count(self, user_uid, table_name, filters):
|
||||||
|
db = await self.get_db(user_uid)
|
||||||
|
if not filters:
|
||||||
|
filters = {}
|
||||||
|
return db[table_name].count(**filters)
|
||||||
+151
-39
@@ -1,41 +1,153 @@
|
|||||||
_H='Documents'
|
|
||||||
_G='Archives'
|
|
||||||
_F='Videos'
|
|
||||||
_E='Pictures'
|
|
||||||
_D='uid'
|
|
||||||
_C='user_uid'
|
|
||||||
_B='My Drive'
|
|
||||||
_A='name'
|
|
||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
|
||||||
class DriveService(BaseService):
|
class DriveService(BaseService):
|
||||||
mapper_name='drive';EXTENSIONS_PICTURES=['jpg','jpeg','png','gif','svg','webp','tiff'];EXTENSIONS_VIDEOS=['mp4','m4v','mov','wmv','webm','mkv','mpg','mpeg','avi','ogv','ogg','flv','3gp','3g2'];EXTENSIONS_ARCHIVES=['zip','rar','7z','tar','tar.gz','tar.xz','tar.bz2','tar.lzma','tar.lz'];EXTENSIONS_AUDIO=['mp3','wav','ogg','flac','m4a','wma','aac','opus','aiff','au','mid','midi'];EXTENSIONS_DOCS=['pdf','doc','docx','xls','xlsx','ppt','pptx','txt','md','json','csv','xml','html','css','js','py','sql','rs','toml','yml','yaml','ini','conf','config','log','csv','tsv','java','cs','csproj','scss','less','sass','json','lock','lock.json','jsonl']
|
|
||||||
async def get_drive_name_by_extension(B,extension):
|
mapper_name = "drive"
|
||||||
A=extension
|
|
||||||
if A.startswith('.'):A=A[1:]
|
EXTENSIONS_PICTURES = ["jpg", "jpeg", "png", "gif", "svg", "webp", "tiff"]
|
||||||
if A in B.EXTENSIONS_PICTURES:return _E
|
EXTENSIONS_VIDEOS = [
|
||||||
if A in B.EXTENSIONS_VIDEOS:return _F
|
"mp4",
|
||||||
if A in B.EXTENSIONS_ARCHIVES:return _G
|
"m4v",
|
||||||
if A in B.EXTENSIONS_AUDIO:return'Audio'
|
"mov",
|
||||||
if A in B.EXTENSIONS_DOCS:return _H
|
"wmv",
|
||||||
return _B
|
"webm",
|
||||||
async def get_drive_by_extension(A,user_uid,extension):B=await A.get_drive_name_by_extension(extension);return await A.get_or_create(user_uid=user_uid,name=B)
|
"mkv",
|
||||||
async def get_by_user(C,user_uid,name=None):
|
"mpg",
|
||||||
B=name;D={_C:user_uid}
|
"mpeg",
|
||||||
async for A in C.find(**D):
|
"avi",
|
||||||
if not B:yield A
|
"ogv",
|
||||||
elif A[_A]==B:yield A
|
"ogg",
|
||||||
elif not A[_A]and B==_B:A[_A]=_B;await C.save(A);yield A
|
"flv",
|
||||||
async def get_or_create(B,user_uid,name=None,extensions=None):
|
"3gp",
|
||||||
D=user_uid;C=name;E={_C:D}
|
"3g2",
|
||||||
if C:E[_A]=C
|
]
|
||||||
async for A in B.get_by_user(**E):return A
|
EXTENSIONS_ARCHIVES = [
|
||||||
A=await B.new();A[_C]=D;A[_A]=C;await B.save(A);return A
|
"zip",
|
||||||
async def prepare_default_drives(B):
|
"rar",
|
||||||
C='drive_uid'
|
"7z",
|
||||||
async for A in B.services.drive_item.find():
|
"tar",
|
||||||
E=A.extension;D=await B.get_drive_by_extension(A[_C],E)
|
"tar.gz",
|
||||||
if not A[C]==D[_D]:A[C]=D[_D];await B.services.drive_item.save(A)
|
"tar.xz",
|
||||||
async def prepare_default_drives_for_user(A,user_uid):B=user_uid;await A.get_or_create(user_uid=B,name=_B);await A.get_or_create(user_uid=B,name='Shared Drive');await A.get_or_create(user_uid=B,name=_E);await A.get_or_create(user_uid=B,name=_F);await A.get_or_create(user_uid=B,name=_G);await A.get_or_create(user_uid=B,name=_H)
|
"tar.bz2",
|
||||||
async def prepare_all(A):
|
"tar.lzma",
|
||||||
await A.prepare_default_drives()
|
"tar.lz",
|
||||||
async for B in A.services.user.find():await A.prepare_default_drives_for_user(B[_D])
|
]
|
||||||
|
EXTENSIONS_AUDIO = [
|
||||||
|
"mp3",
|
||||||
|
"wav",
|
||||||
|
"ogg",
|
||||||
|
"flac",
|
||||||
|
"m4a",
|
||||||
|
"wma",
|
||||||
|
"aac",
|
||||||
|
"opus",
|
||||||
|
"aiff",
|
||||||
|
"au",
|
||||||
|
"mid",
|
||||||
|
"midi",
|
||||||
|
]
|
||||||
|
EXTENSIONS_DOCS = [
|
||||||
|
"pdf",
|
||||||
|
"doc",
|
||||||
|
"docx",
|
||||||
|
"xls",
|
||||||
|
"xlsx",
|
||||||
|
"ppt",
|
||||||
|
"pptx",
|
||||||
|
"txt",
|
||||||
|
"md",
|
||||||
|
"json",
|
||||||
|
"csv",
|
||||||
|
"xml",
|
||||||
|
"html",
|
||||||
|
"css",
|
||||||
|
"js",
|
||||||
|
"py",
|
||||||
|
"sql",
|
||||||
|
"rs",
|
||||||
|
"toml",
|
||||||
|
"yml",
|
||||||
|
"yaml",
|
||||||
|
"ini",
|
||||||
|
"conf",
|
||||||
|
"config",
|
||||||
|
"log",
|
||||||
|
"csv",
|
||||||
|
"tsv",
|
||||||
|
"java",
|
||||||
|
"cs",
|
||||||
|
"csproj",
|
||||||
|
"scss",
|
||||||
|
"less",
|
||||||
|
"sass",
|
||||||
|
"json",
|
||||||
|
"lock",
|
||||||
|
"lock.json",
|
||||||
|
"jsonl",
|
||||||
|
]
|
||||||
|
|
||||||
|
async def get_drive_name_by_extension(self, extension):
|
||||||
|
if extension.startswith("."):
|
||||||
|
extension = extension[1:]
|
||||||
|
if extension in self.EXTENSIONS_PICTURES:
|
||||||
|
return "Pictures"
|
||||||
|
if extension in self.EXTENSIONS_VIDEOS:
|
||||||
|
return "Videos"
|
||||||
|
if extension in self.EXTENSIONS_ARCHIVES:
|
||||||
|
return "Archives"
|
||||||
|
if extension in self.EXTENSIONS_AUDIO:
|
||||||
|
return "Audio"
|
||||||
|
if extension in self.EXTENSIONS_DOCS:
|
||||||
|
return "Documents"
|
||||||
|
return "My Drive"
|
||||||
|
|
||||||
|
async def get_drive_by_extension(self, user_uid, extension):
|
||||||
|
name = await self.get_drive_name_by_extension(extension)
|
||||||
|
return await self.get_or_create(user_uid=user_uid, name=name)
|
||||||
|
|
||||||
|
async def get_by_user(self, user_uid, name=None):
|
||||||
|
kwargs = {"user_uid": user_uid}
|
||||||
|
async for model in self.find(**kwargs):
|
||||||
|
if not name:
|
||||||
|
yield model
|
||||||
|
elif model["name"] == name:
|
||||||
|
yield model
|
||||||
|
elif not model["name"] and name == "My Drive":
|
||||||
|
model["name"] = "My Drive"
|
||||||
|
await self.save(model)
|
||||||
|
yield model
|
||||||
|
|
||||||
|
async def get_or_create(self, user_uid, name=None, extensions=None):
|
||||||
|
kwargs = {"user_uid": user_uid}
|
||||||
|
if name:
|
||||||
|
kwargs["name"] = name
|
||||||
|
async for model in self.get_by_user(**kwargs):
|
||||||
|
return model
|
||||||
|
|
||||||
|
model = await self.new()
|
||||||
|
model["user_uid"] = user_uid
|
||||||
|
model["name"] = name
|
||||||
|
await self.save(model)
|
||||||
|
return model
|
||||||
|
|
||||||
|
async def prepare_default_drives(self):
|
||||||
|
async for drive_item in self.services.drive_item.find():
|
||||||
|
extension = drive_item.extension
|
||||||
|
drive = await self.get_drive_by_extension(drive_item["user_uid"], extension)
|
||||||
|
if not drive_item["drive_uid"] == drive["uid"]:
|
||||||
|
drive_item["drive_uid"] = drive["uid"]
|
||||||
|
await self.services.drive_item.save(drive_item)
|
||||||
|
|
||||||
|
async def prepare_default_drives_for_user(self, user_uid):
|
||||||
|
await self.get_or_create(user_uid=user_uid, name="My Drive")
|
||||||
|
await self.get_or_create(user_uid=user_uid, name="Shared Drive")
|
||||||
|
await self.get_or_create(user_uid=user_uid, name="Pictures")
|
||||||
|
await self.get_or_create(user_uid=user_uid, name="Videos")
|
||||||
|
await self.get_or_create(user_uid=user_uid, name="Archives")
|
||||||
|
await self.get_or_create(user_uid=user_uid, name="Documents")
|
||||||
|
|
||||||
|
async def prepare_all(self):
|
||||||
|
await self.prepare_default_drives()
|
||||||
|
async for user in self.services.user.find():
|
||||||
|
await self.prepare_default_drives_for_user(user["uid"])
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
|
||||||
class DriveItemService(BaseService):
|
class DriveItemService(BaseService):
|
||||||
mapper_name='drive_item'
|
|
||||||
async def create(B,drive_uid,name,path,type_,size):
|
mapper_name = "drive_item"
|
||||||
A=await B.new();A['drive_uid']=drive_uid;A['name']=name;A['path']=str(path);A['extension']=str(name).split('.')[-1];A['file_type']=type_;A['file_size']=size
|
|
||||||
if await B.save(A):return A
|
async def create(self, drive_uid, name, path, type_, size):
|
||||||
C=await A.errors;raise Exception(f"Failed to create drive item: {C}.")
|
model = await self.new()
|
||||||
|
model["drive_uid"] = drive_uid
|
||||||
|
model["name"] = name
|
||||||
|
model["path"] = str(path)
|
||||||
|
model["extension"] = str(name).split(".")[-1]
|
||||||
|
model["file_type"] = type_
|
||||||
|
model["file_size"] = size
|
||||||
|
if await self.save(model):
|
||||||
|
return model
|
||||||
|
errors = await model.errors
|
||||||
|
print("XXXXXXXXXX")
|
||||||
|
raise Exception(f"Failed to create drive item: {errors}.")
|
||||||
|
|||||||
@@ -1,28 +1,65 @@
|
|||||||
_E='message'
|
|
||||||
_D='object_type'
|
|
||||||
_C='object_uid'
|
|
||||||
_B=False
|
|
||||||
_A='user_uid'
|
|
||||||
from snek.system.model import now
|
from snek.system.model import now
|
||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
|
||||||
class NotificationService(BaseService):
|
class NotificationService(BaseService):
|
||||||
mapper_name='notification'
|
mapper_name = "notification"
|
||||||
async def mark_as_read(B,user_uid,channel_message_uid):
|
|
||||||
A=await B.get(user_uid,object_uid=channel_message_uid)
|
async def mark_as_read(self, user_uid, channel_message_uid):
|
||||||
if not A:return _B
|
model = await self.get(user_uid, object_uid=channel_message_uid)
|
||||||
A['read_at']=now();await B.save(A);return True
|
if not model:
|
||||||
async def get_unread_stats(A,user_uid):await A.query('SELECT object_type, COUNT(*) as count FROM notification WHERE user_uid=:user_uid AND read_at IS NULL GROUP BY object_type',{_A:user_uid})
|
return False
|
||||||
async def create(B,object_uid,object_type,user_uid,message):
|
model["read_at"] = now()
|
||||||
A=await B.new();A[_C]=object_uid;A[_D]=object_type;A[_A]=user_uid;A[_E]=message
|
await self.save(model)
|
||||||
if await B.save(A):return A
|
return True
|
||||||
raise Exception(f"Failed to create notification: {A.errors}.")
|
|
||||||
async def create_channel_message(A,channel_message_uid):
|
async def get_unread_stats(self, user_uid):
|
||||||
E=channel_message_uid;D='new_count';F=await A.services.channel_message.get(uid=E);G=await A.services.user.get(uid=F[_A]);A.app.db.begin()
|
await self.query(
|
||||||
async for B in A.services.channel_member.find(channel_uid=F['channel_uid'],is_banned=_B,is_muted=_B,deleted_at=None):
|
"SELECT object_type, COUNT(*) as count FROM notification WHERE user_uid=:user_uid AND read_at IS NULL GROUP BY object_type",
|
||||||
if not B[D]:B[D]=0
|
{"user_uid": user_uid},
|
||||||
B[D]+=1;H=await A.services.user.get(uid=B[_A])
|
)
|
||||||
if not H:continue
|
|
||||||
await A.services.channel_member.save(B);C=await A.new();C[_C]=E;C[_D]='channel_message';C[_A]=B[_A];C[_E]=f"New message from {G["nick"]} in {B["label"]}."
|
async def create(self, object_uid, object_type, user_uid, message):
|
||||||
try:await A.save(C)
|
model = await self.new()
|
||||||
except Exception:raise Exception(f"Failed to create notification: {C.errors}.")
|
model["object_uid"] = object_uid
|
||||||
A.app.db.commit()
|
model["object_type"] = object_type
|
||||||
|
model["user_uid"] = user_uid
|
||||||
|
model["message"] = message
|
||||||
|
if await self.save(model):
|
||||||
|
return model
|
||||||
|
raise Exception(f"Failed to create notification: {model.errors}.")
|
||||||
|
|
||||||
|
async def create_channel_message(self, channel_message_uid):
|
||||||
|
channel_message = await self.services.channel_message.get(
|
||||||
|
uid=channel_message_uid
|
||||||
|
)
|
||||||
|
user = await self.services.user.get(uid=channel_message["user_uid"])
|
||||||
|
self.app.db.begin()
|
||||||
|
async for channel_member in self.services.channel_member.find(
|
||||||
|
channel_uid=channel_message["channel_uid"],
|
||||||
|
is_banned=False,
|
||||||
|
is_muted=False,
|
||||||
|
deleted_at=None,
|
||||||
|
):
|
||||||
|
if not channel_member["new_count"]:
|
||||||
|
channel_member["new_count"] = 0
|
||||||
|
channel_member["new_count"] += 1
|
||||||
|
|
||||||
|
usr = await self.services.user.get(uid=channel_member["user_uid"])
|
||||||
|
if not usr:
|
||||||
|
continue
|
||||||
|
await self.services.channel_member.save(channel_member)
|
||||||
|
|
||||||
|
model = await self.new()
|
||||||
|
model["object_uid"] = channel_message_uid
|
||||||
|
model["object_type"] = "channel_message"
|
||||||
|
model["user_uid"] = channel_member["user_uid"]
|
||||||
|
model["message"] = (
|
||||||
|
f"New message from {user['nick']} in {channel_member['label']}."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await self.save(model)
|
||||||
|
except Exception:
|
||||||
|
raise Exception(f"Failed to create notification: {model.errors}.")
|
||||||
|
|
||||||
|
self.app.db.commit()
|
||||||
|
|||||||
@@ -1,23 +1,52 @@
|
|||||||
_B='user_uid'
|
|
||||||
_A=False
|
|
||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
import asyncio,shutil
|
import asyncio
|
||||||
|
import shutil
|
||||||
|
|
||||||
class RepositoryService(BaseService):
|
class RepositoryService(BaseService):
|
||||||
mapper_name='repository'
|
mapper_name = "repository"
|
||||||
async def delete(B,user_uid,name):
|
|
||||||
A=user_uid;C=asyncio.get_event_loop();D=(await B.services.user.get_repository_path(A)).joinpath(name)
|
async def delete(self, user_uid, name):
|
||||||
try:await C.run_in_executor(None,shutil.rmtree,D)
|
loop = asyncio.get_event_loop()
|
||||||
except Exception as E:print(E)
|
repository_path = (await self.services.user.get_repository_path(user_uid)).joinpath(name)
|
||||||
await super().delete(user_uid=A,name=name)
|
try:
|
||||||
async def exists(B,user_uid,name,**A):A[_B]=user_uid;A['name']=name;return await super().exists(**A)
|
await loop.run_in_executor(None, shutil.rmtree, repository_path)
|
||||||
async def init(D,user_uid,name):
|
except Exception as ex:
|
||||||
B='.git';A=await D.services.user.get_repository_path(user_uid)
|
print(ex)
|
||||||
if not A.exists():A.mkdir(parents=True)
|
|
||||||
A=A.joinpath(name);A=str(A)
|
await super().delete(user_uid=user_uid, name=name)
|
||||||
if not A.endswith(B):A+=B
|
|
||||||
E=['git','init','--bare',A];C=await asyncio.subprocess.create_subprocess_exec(*E,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.PIPE);F,G=await C.communicate();return C.returncode==0
|
|
||||||
async def create(A,user_uid,name,is_private=_A):
|
async def exists(self, user_uid, name, **kwargs):
|
||||||
C=name;D=user_uid
|
kwargs["user_uid"] = user_uid
|
||||||
if await A.exists(user_uid=D,name=C):return _A
|
kwargs["name"] = name
|
||||||
if not await A.init(user_uid=D,name=C):return _A
|
return await super().exists(**kwargs)
|
||||||
B=await A.new();B[_B]=D;B['name']=C;B['is_private']=is_private;return await A.save(B)
|
|
||||||
|
async def init(self, user_uid, name):
|
||||||
|
repository_path = await self.services.user.get_repository_path(user_uid)
|
||||||
|
if not repository_path.exists():
|
||||||
|
repository_path.mkdir(parents=True)
|
||||||
|
repository_path = repository_path.joinpath(name)
|
||||||
|
repository_path = str(repository_path)
|
||||||
|
if not repository_path.endswith(".git"):
|
||||||
|
repository_path += ".git"
|
||||||
|
command = ['git', 'init', '--bare', repository_path]
|
||||||
|
process = await asyncio.subprocess.create_subprocess_exec(
|
||||||
|
*command,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
stdout, stderr = await process.communicate()
|
||||||
|
return process.returncode == 0
|
||||||
|
|
||||||
|
async def create(self, user_uid, name,is_private=False):
|
||||||
|
if await self.exists(user_uid=user_uid, name=name):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not await self.init(user_uid=user_uid, name=name):
|
||||||
|
return False
|
||||||
|
|
||||||
|
model = await self.new()
|
||||||
|
model["user_uid"] = user_uid
|
||||||
|
model["name"] = name
|
||||||
|
model["is_private"] = is_private
|
||||||
|
return await self.save(model)
|
||||||
|
|||||||
+100
-31
@@ -1,36 +1,105 @@
|
|||||||
_B=False
|
|
||||||
_A=True
|
|
||||||
from snek.model.user import UserModel
|
from snek.model.user import UserModel
|
||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
from snek.system.model import now
|
||||||
|
|
||||||
class SocketService(BaseService):
|
class SocketService(BaseService):
|
||||||
|
|
||||||
class Socket:
|
class Socket:
|
||||||
def __init__(A,ws,user):A.ws=ws;A.is_connected=_A;A.user=user
|
def __init__(self, ws, user: UserModel):
|
||||||
async def send_json(A,data):
|
self.ws = ws
|
||||||
if not A.is_connected:return _B
|
self.is_connected = True
|
||||||
try:await A.ws.send_json(data)
|
self.user = user
|
||||||
except Exception:A.is_connected=_B
|
|
||||||
return A.is_connected
|
|
||||||
async def close(A):
|
async def send_json(self, data):
|
||||||
if not A.is_connected:return _A
|
if not self.is_connected:
|
||||||
await A.ws.close();A.is_connected=_B;return _A
|
return False
|
||||||
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:
|
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)
|
await self.ws.send_json(data)
|
||||||
except Exception as C:print(C,flush=_A)
|
except Exception:
|
||||||
return _A
|
self.is_connected = False
|
||||||
async def delete(A,ws):
|
return self.is_connected
|
||||||
for B in[A for A in A.sockets if A.ws==ws]:await B.close();A.sockets.remove(B)
|
|
||||||
|
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 = {}
|
||||||
|
self.last_update = str(datetime.now())
|
||||||
|
|
||||||
|
|
||||||
|
async def user_availability_service(self):
|
||||||
|
logger.info("User availability update service started.")
|
||||||
|
while True:
|
||||||
|
logger.info("Updating user availability...")
|
||||||
|
users_updated = []
|
||||||
|
for s in self.sockets:
|
||||||
|
if not s.is_connected:
|
||||||
|
continue
|
||||||
|
if not s.user in users_updated:
|
||||||
|
s.user["last_ping"] = now()
|
||||||
|
await self.app.services.user.save(s.user)
|
||||||
|
users_updated.append(s.user)
|
||||||
|
logger.info(f"Updated user availability for {len(users_updated)} online users.")
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
|
||||||
|
async def add(self, ws, user_uid):
|
||||||
|
s = self.Socket(ws, await self.app.services.user.get(uid=user_uid))
|
||||||
|
self.sockets.add(s)
|
||||||
|
s.user["last_ping"] = now()
|
||||||
|
await self.app.services.user.save(s.user)
|
||||||
|
logger.info(f"Added socket for user {s.user['username']}")
|
||||||
|
if not self.users.get(user_uid):
|
||||||
|
self.users[user_uid] = set()
|
||||||
|
self.users[user_uid].add(s)
|
||||||
|
|
||||||
|
async def subscribe(self, ws, channel_uid, user_uid):
|
||||||
|
if channel_uid not in self.subscriptions:
|
||||||
|
self.subscriptions[channel_uid] = set()
|
||||||
|
s = self.Socket(ws, await self.app.services.user.get(uid=user_uid))
|
||||||
|
self.subscriptions[channel_uid].add(s)
|
||||||
|
|
||||||
|
async def send_to_user(self, user_uid, message):
|
||||||
|
count = 0
|
||||||
|
for s in self.users.get(user_uid, []):
|
||||||
|
if await s.send_json(message):
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
async def broadcast(self, channel_uid, message):
|
||||||
|
await self._broadcast(channel_uid, message)
|
||||||
|
|
||||||
|
async def _broadcast(self, channel_uid, message):
|
||||||
|
sent = 0
|
||||||
|
try:
|
||||||
|
async for user_uid in self.services.channel_member.get_user_uids(
|
||||||
|
channel_uid
|
||||||
|
):
|
||||||
|
sent += await self.send_to_user(user_uid, message)
|
||||||
|
except Exception as ex:
|
||||||
|
print(ex, flush=True)
|
||||||
|
logger.info(f"Broadcasted a message to {sent} users.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def delete(self, ws):
|
||||||
|
for s in [sock for sock in self.sockets if sock.ws == ws]:
|
||||||
|
await s.close()
|
||||||
|
logger.info(f"Removed socket for user {s.user['username']}")
|
||||||
|
self.sockets.remove(s)
|
||||||
|
|
||||||
|
|||||||
+108
-49
@@ -1,53 +1,112 @@
|
|||||||
_B='color'
|
|
||||||
_A=True
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
from snek.system import security
|
from snek.system import security
|
||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
|
||||||
class UserService(BaseService):
|
class UserService(BaseService):
|
||||||
mapper_name='user'
|
mapper_name = "user"
|
||||||
async def get_by_username(A,username):return await A.get(username=username)
|
|
||||||
async def search(C,query,**D):
|
async def get_by_username(self, username):
|
||||||
A=query;A=A.strip().lower()
|
return await self.get(username=username)
|
||||||
if not A:return[]
|
|
||||||
B=[]
|
async def search(self, query, **kwargs):
|
||||||
async for E in C.find(username={'ilike':'%'+A+'%'},**D):B.append(E)
|
query = query.strip().lower()
|
||||||
return B
|
if not query:
|
||||||
async def validate_login(C,username,password):
|
return []
|
||||||
A=False;B=await C.get(username=username)
|
results = []
|
||||||
if not B:return A
|
async for result in self.find(username={"ilike": "%" + query + "%"}, **kwargs):
|
||||||
if not await security.verify(password,B['password']):return A
|
results.append(result)
|
||||||
return _A
|
return results
|
||||||
async def save(B,user):
|
|
||||||
A=user
|
async def validate_login(self, username, password):
|
||||||
if not A[_B]:A[_B]=await B.services.util.random_light_hex_color()
|
model = await self.get(username=username)
|
||||||
return await super().save(A)
|
if not model:
|
||||||
async def authenticate(B,username,password):
|
return False
|
||||||
C=password;A=username;print(A,C,flush=_A);D=await B.validate_login(A,C);print(D,flush=_A)
|
if not await security.verify(password, model["password"]):
|
||||||
if not D:return
|
return False
|
||||||
E=await B.get(username=A,deleted_at=None);return E
|
return True
|
||||||
def get_admin_uids(A):return A.mapper.get_admin_uids()
|
|
||||||
async def get_repository_path(A,user_uid):return pathlib.Path(f"./drive/repositories/{user_uid}")
|
async def save(self, user):
|
||||||
async def get_static_path(B,user_uid):
|
if not user["color"]:
|
||||||
A=pathlib.Path(f"./drive/{user_uid}/snek/static")
|
user["color"] = await self.services.util.random_light_hex_color()
|
||||||
if not A.exists():return
|
return await super().save(user)
|
||||||
return A
|
|
||||||
async def get_template_path(B,user_uid):
|
def authenticate_sync(self,username,password):
|
||||||
A=pathlib.Path(f"./drive/{user_uid}/snek/templates")
|
user = self.get_by_username_sync(username)
|
||||||
if not A.exists():return
|
|
||||||
return A
|
if not user:
|
||||||
async def get_home_folder(B,user_uid):
|
return False
|
||||||
A=pathlib.Path(f"./drive/{user_uid}")
|
if not security.verify_sync(password, user["password"]):
|
||||||
if not A.exists():
|
return False
|
||||||
try:A.mkdir(parents=_A,exist_ok=_A)
|
return True
|
||||||
except:pass
|
|
||||||
return A
|
async def authenticate(self, username, password):
|
||||||
async def register(B,email,username,password):
|
success = await self.validate_login(username, password)
|
||||||
C=username
|
if not success:
|
||||||
if await B.exists(username=C):raise Exception('User already exists.')
|
return None
|
||||||
A=await B.new();A['nick']=C;A[_B]=await B.services.util.random_light_hex_color();A.email.value=email;A.username.value=C;A.password.value=await security.hash(password)
|
|
||||||
if await B.save(A):
|
model = await self.get(username=username, deleted_at=None)
|
||||||
if A:
|
return model
|
||||||
D=await B.services.channel.ensure_public_channel(A['uid'])
|
|
||||||
if not D:raise Exception('Failed to create public channel.')
|
def get_admin_uids(self):
|
||||||
return A
|
return self.mapper.get_admin_uids()
|
||||||
raise Exception(f"Failed to create user: {A.errors}.")
|
|
||||||
|
async def get_repository_path(self, user_uid):
|
||||||
|
return pathlib.Path(f"./drive/repositories/{user_uid}")
|
||||||
|
|
||||||
|
async def get_static_path(self, user_uid):
|
||||||
|
path = pathlib.Path(f"./drive/{user_uid}/snek/static")
|
||||||
|
if not path.exists():
|
||||||
|
return None
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def get_template_path(self, user_uid):
|
||||||
|
path = pathlib.Path(f"./drive/{user_uid}/snek/templates")
|
||||||
|
if not path.exists():
|
||||||
|
return None
|
||||||
|
return path
|
||||||
|
|
||||||
|
def get_by_username_sync(self, username):
|
||||||
|
user = self.mapper.db["user"].find_one(username=username, deleted_at=None)
|
||||||
|
return dict(user)
|
||||||
|
|
||||||
|
def get_home_folder_by_username(self, username):
|
||||||
|
user = self.get_by_username_sync(username)
|
||||||
|
folder = pathlib.Path(f"./drive/{user['uid']}")
|
||||||
|
if not folder.exists():
|
||||||
|
try:
|
||||||
|
folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return folder
|
||||||
|
|
||||||
|
async def get_home_folder(self, user_uid):
|
||||||
|
folder = pathlib.Path(f"./drive/{user_uid}")
|
||||||
|
if not folder.exists():
|
||||||
|
try:
|
||||||
|
folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return folder
|
||||||
|
|
||||||
|
async def register(self, email, username, password):
|
||||||
|
if await self.exists(username=username):
|
||||||
|
raise Exception("User already exists.")
|
||||||
|
model = await self.new()
|
||||||
|
model["nick"] = username
|
||||||
|
model["color"] = await self.services.util.random_light_hex_color()
|
||||||
|
model.email.value = email
|
||||||
|
model.username.value = username
|
||||||
|
model.password.value = await security.hash(password)
|
||||||
|
if await self.save(model):
|
||||||
|
if model:
|
||||||
|
channel = await self.services.channel.ensure_public_channel(
|
||||||
|
model["uid"]
|
||||||
|
)
|
||||||
|
if not channel:
|
||||||
|
raise Exception("Failed to create public channel.")
|
||||||
|
return model
|
||||||
|
raise Exception(f"Failed to create user: {model.errors}.")
|
||||||
|
|||||||
@@ -1,15 +1,35 @@
|
|||||||
_A='user_property'
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
|
||||||
class UserPropertyService(BaseService):
|
class UserPropertyService(BaseService):
|
||||||
mapper_name=_A
|
mapper_name = "user_property"
|
||||||
async def set(C,user_uid,name,value):A='name';B='user_uid';C.mapper.db[_A].upsert({B:user_uid,A:name,'value':json.dumps(value,default=str)},[B,A])
|
|
||||||
async def get(B,user_uid,name):
|
async def set(self, user_uid, name, value):
|
||||||
try:return json.loads((await super().get(user_uid=user_uid,name=name))['value'])
|
self.mapper.db["user_property"].upsert(
|
||||||
except Exception as A:print(A);return
|
{
|
||||||
async def search(C,query,**D):
|
"user_uid": user_uid,
|
||||||
A=query;A=A.strip().lower()
|
"name": name,
|
||||||
if not A:raise[]
|
"value": json.dumps(value, default=str),
|
||||||
B=[]
|
},
|
||||||
async for E in C.find(name={'ilike':'%'+A+'%'},**D):B.append(E)
|
["user_uid", "name"],
|
||||||
return B
|
)
|
||||||
|
|
||||||
|
async def get(self, user_uid, name):
|
||||||
|
try:
|
||||||
|
return json.loads(
|
||||||
|
(await super().get(user_uid=user_uid, name=name))["value"]
|
||||||
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
print(ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def search(self, query, **kwargs):
|
||||||
|
query = query.strip().lower()
|
||||||
|
if not query:
|
||||||
|
raise []
|
||||||
|
results = []
|
||||||
|
async for result in self.find(name={"ilike": "%" + query + "%"}, **kwargs):
|
||||||
|
results.append(result)
|
||||||
|
return results
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
|
||||||
class UtilService(BaseService):
|
class UtilService(BaseService):
|
||||||
async def random_light_hex_color(D):A=random.randint(128,255);B=random.randint(128,255);C=random.randint(128,255);return f"#{A:02x}{B:02x}{C:02x}"
|
|
||||||
|
async def random_light_hex_color(self):
|
||||||
|
|
||||||
|
r = random.randint(128, 255)
|
||||||
|
g = random.randint(128, 255)
|
||||||
|
b = random.randint(128, 255)
|
||||||
|
|
||||||
|
return f"#{r:02x}{g:02x}{b:02x}"
|
||||||
|
|||||||
+445
-166
@@ -1,207 +1,486 @@
|
|||||||
_O='branches'
|
import os
|
||||||
_N='message'
|
import aiohttp
|
||||||
_M='author'
|
|
||||||
_L='Invalid JSON data'
|
|
||||||
_K='origin'
|
|
||||||
_J='Repository not found'
|
|
||||||
_I='main'
|
|
||||||
_H='repository'
|
|
||||||
_G='branch'
|
|
||||||
_F='.git'
|
|
||||||
_E=None
|
|
||||||
_D='user'
|
|
||||||
_C='repo_name'
|
|
||||||
_B='username'
|
|
||||||
_A='repository_path'
|
|
||||||
import os,aiohttp
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import git,shutil,json,tempfile,asyncio,logging,base64,pathlib
|
|
||||||
|
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')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
logger = logging.getLogger('git_server')
|
logger = logging.getLogger('git_server')
|
||||||
|
|
||||||
class GitApplication(web.Application):
|
class GitApplication(web.Application):
|
||||||
def __init__(A,parent=_E):B='/branches/{repo_name}';A.parent=parent;super().__init__(client_max_size=5368709120);A.REPO_DIR='drive/repositories/3177f85e-dbb3-4406-993e-3d3748fea545';A.USERS={'x':'x','bob':'bobpass'};A.add_routes([web.post('/create/{repo_name}',A.create_repository),web.delete('/delete/{repo_name}',A.delete_repository),web.get('/clone/{repo_name}',A.clone_repository),web.post('/push/{repo_name}',A.push_repository),web.post('/pull/{repo_name}',A.pull_repository),web.get('/status/{repo_name}',A.status_repository),web.get('/list',A.list_repositories),web.get(B,A.list_branches),web.post(B,A.create_branch),web.get('/log/{repo_name}',A.commit_log),web.get('/file/{repo_name}/{file_path:.*}',A.file_content),web.get('/{path:.+}/info/refs',A.git_smart_http),web.post('/{path:.+}/git-upload-pack',A.git_smart_http),web.post('/{path:.+}/git-receive-pack',A.git_smart_http),web.get('/{repo_name}.git/info/refs',A.git_smart_http),web.post('/{repo_name}.git/git-upload-pack',A.git_smart_http),web.post('/{repo_name}.git/git-receive-pack',A.git_smart_http)])
|
def __init__(self, parent=None):
|
||||||
async def check_basic_auth(B,request):
|
#import git
|
||||||
C='Basic ';A=request;D=A.headers.get('Authorization','')
|
#globals()['git'] = git
|
||||||
if not D.startswith(C):return _E,_E
|
self.parent = parent
|
||||||
E=D.split(C)[1];F=base64.b64decode(E).decode();G,H=F.split(':',1);A[_D]=await B.parent.services.user.authenticate(username=G,password=H)
|
super().__init__(client_max_size=1024*1024*1024*5)
|
||||||
if not A[_D]:return _E,_E
|
self.add_routes([
|
||||||
A[_A]=await B.parent.services.user.get_repository_path(A[_D]['uid']);return A[_D][_B],A[_A]
|
web.post('/create/{repo_name}', self.create_repository),
|
||||||
|
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
|
@staticmethod
|
||||||
def require_auth(handler):
|
def require_auth(handler):
|
||||||
async def A(self,request,*D,**E):
|
async def wrapped(self, request, *args, **kwargs):
|
||||||
A=request;B,C=await self.check_basic_auth(A)
|
username, repository_path = await self.check_basic_auth(request)
|
||||||
if not B or not C:return web.Response(status=401,headers={'WWW-Authenticate':'Basic'},text='Authentication required')
|
if not username or not repository_path:
|
||||||
A[_B]=B;A[_A]=C;return await handler(self,A,*D,**E)
|
return web.Response(status=401, headers={'WWW-Authenticate': 'Basic'}, text='Authentication required')
|
||||||
return A
|
request['username'] = username
|
||||||
def repo_path(A,repository_path,repo_name):return repository_path.joinpath(repo_name+_F)
|
request['repository_path'] = repository_path
|
||||||
def check_repo_exists(A,repository_path,repo_name):
|
return await handler(self, request, *args, **kwargs)
|
||||||
B=A.repo_path(repository_path,repo_name)
|
return wrapped
|
||||||
if not os.path.exists(B):return web.Response(text=_J,status=404)
|
|
||||||
|
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
|
@require_auth
|
||||||
async def create_repository(self, request):
|
async def create_repository(self, request):
|
||||||
B=request;E=B[_B];A=B.match_info[_C];F=B[_A]
|
username = request['username']
|
||||||
if not A or'/'in A or'..'in A:return web.Response(text='Invalid repository name',status=400)
|
repo_name = request.match_info['repo_name']
|
||||||
C=self.repo_path(F,A)
|
repository_path = request['repository_path']
|
||||||
if os.path.exists(C):return web.Response(text='Repository already exists',status=400)
|
if not repo_name or '/' in repo_name or '..' in repo_name:
|
||||||
try:git.Repo.init(C,bare=True);logger.info(f"Created repository: {A} for user {E}");return web.Response(text=f"Created repository {A}")
|
return web.Response(text="Invalid repository name", status=400)
|
||||||
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)
|
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
|
@require_auth
|
||||||
async def delete_repository(self, request):
|
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)
|
username = request['username']
|
||||||
if D:return D
|
repo_name = request.match_info['repo_name']
|
||||||
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}")
|
repository_path = request['repository_path']
|
||||||
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)
|
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
|
@require_auth
|
||||||
async def clone_repository(self, request):
|
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)
|
username = request['username']
|
||||||
if C:return C
|
repo_name = request.match_info['repo_name']
|
||||||
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)
|
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
|
@require_auth
|
||||||
async def push_repository(self, request):
|
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)
|
username = request['username']
|
||||||
if F:return F
|
repo_name = request.match_info['repo_name']
|
||||||
try:D=await B.json()
|
repository_path = request['repository_path']
|
||||||
except json.JSONDecodeError:return web.Response(text=_L,status=400)
|
error_response = self.check_repo_exists(repository_path, repo_name)
|
||||||
M=D.get('commit_message','Update from server');G=D.get(_G,_I);H=D.get('changes',[])
|
if error_response:
|
||||||
if not H:return web.Response(text='No changes provided',status=400)
|
return error_response
|
||||||
with tempfile.TemporaryDirectory()as I:
|
try:
|
||||||
A=git.Repo.clone_from(self.repo_path(E,C),I)
|
data = await request.json()
|
||||||
for J in H:
|
except json.JSONDecodeError:
|
||||||
K=os.path.join(I,J.get('file',''));N=J.get('content','');os.makedirs(os.path.dirname(K),exist_ok=True)
|
return web.Response(text="Invalid JSON data", status=400)
|
||||||
with open(K,'w')as O:O.write(N)
|
commit_message = data.get('commit_message', 'Update from server')
|
||||||
A.git.add(A=True)
|
branch = data.get('branch', 'main')
|
||||||
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()
|
changes = data.get('changes', [])
|
||||||
A.index.commit(M);P=A.remote(_K);P.push(refspec=f"{G}:{G}")
|
if not changes:
|
||||||
logger.info(f"Pushed to repository: {C} for user {L}");return web.Response(text=f"Successfully pushed changes to {C}")
|
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
|
@require_auth
|
||||||
async def pull_repository(self, request):
|
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)
|
username = request['username']
|
||||||
if I:return I
|
repo_name = request.match_info['repo_name']
|
||||||
try:E=await C.json()
|
repository_path = request['repository_path']
|
||||||
except json.JSONDecodeError:E={}
|
error_response = self.check_repo_exists(repository_path, repo_name)
|
||||||
B=E.get('remote_url');L=E.get(_G,_I)
|
if error_response:
|
||||||
if not B:return web.Response(text='Remote URL is required',status=400)
|
return error_response
|
||||||
with tempfile.TemporaryDirectory()as M:
|
|
||||||
try:
|
try:
|
||||||
D=git.Repo.clone_from(self.repo_path(H,A),M);F='pull_source'
|
data = await request.json()
|
||||||
try:G=D.create_remote(F,B)
|
except json.JSONDecodeError:
|
||||||
except git.GitCommandError:G=D.remote(F);G.set_url(B)
|
data = {}
|
||||||
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}")
|
remote_url = data.get('remote_url')
|
||||||
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)
|
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
|
@require_auth
|
||||||
async def status_repository(self, request):
|
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)
|
username = request['username']
|
||||||
if G:return G
|
repo_name = request.match_info['repo_name']
|
||||||
with tempfile.TemporaryDirectory()as D:
|
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:
|
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=[]
|
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
||||||
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})
|
branches = [b.name for b in temp_repo.branches]
|
||||||
I=[]
|
active_branch = temp_repo.active_branch.name
|
||||||
for(J,T,N)in os.walk(D):
|
commits = []
|
||||||
if _F in J:continue
|
for commit in list(temp_repo.iter_commits(max_count=5)):
|
||||||
for O in N:P=os.path.join(J,O);Q=os.path.relpath(P,D);I.append(Q)
|
commits.append({
|
||||||
R={_H:B,_O:L,'active_branch':M,'recent_commits':H,'files':I};return web.json_response(R)
|
"id": commit.hexsha,
|
||||||
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)
|
"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
|
@require_auth
|
||||||
async def list_repositories(self, request):
|
async def list_repositories(self, request):
|
||||||
D=request;G=D[_B]
|
username = request['username']
|
||||||
try:
|
try:
|
||||||
A=[];B=self.REPO_DIR
|
repos = []
|
||||||
if os.path.exists(B):
|
user_dir = self.REPO_DIR
|
||||||
for C in os.listdir(B):
|
if os.path.exists(user_dir):
|
||||||
F=os.path.join(B,C)
|
for item in os.listdir(user_dir):
|
||||||
if os.path.isdir(F)and C.endswith(_F):A.append(C[:-4])
|
item_path = os.path.join(user_dir, item)
|
||||||
if D.query.get('format')=='json':return web.json_response({'repositories':A})
|
if os.path.isdir(item_path) and item.endswith('.git'):
|
||||||
else:return web.Response(text='\n'.join(A)if A else'No repositories found')
|
repos.append(item[:-4])
|
||||||
except Exception as E:logger.error(f"Error listing repositories: {str(E)}");return web.Response(text=f"Error listing repositories: {str(E)}",status=500)
|
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
|
@require_auth
|
||||||
async def list_branches(self, request):
|
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)
|
username = request['username']
|
||||||
if D:return D
|
repo_name = request.match_info['repo_name']
|
||||||
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})
|
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
|
@require_auth
|
||||||
async def create_branch(self, request):
|
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)
|
username = request['username']
|
||||||
if E:return E
|
repo_name = request.match_info['repo_name']
|
||||||
try:F=await B.json()
|
repository_path = request['repository_path']
|
||||||
except json.JSONDecodeError:return web.Response(text=_L,status=400)
|
error_response = self.check_repo_exists(repository_path, repo_name)
|
||||||
A=F.get('branch_name');J=F.get('start_point','HEAD')
|
if error_response:
|
||||||
if not A:return web.Response(text='Branch name is required',status=400)
|
return error_response
|
||||||
with tempfile.TemporaryDirectory()as K:
|
try:
|
||||||
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}")
|
data = await request.json()
|
||||||
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)
|
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
|
@require_auth
|
||||||
async def commit_log(self, request):
|
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)
|
username = request['username']
|
||||||
if G:return G
|
repo_name = request.match_info['repo_name']
|
||||||
try:I=int(B.query.get('limit',10));H=B.query.get(_G,_I)
|
repository_path = request['repository_path']
|
||||||
except ValueError:return web.Response(text='Invalid limit parameter',status=400)
|
error_response = self.check_repo_exists(repository_path, repo_name)
|
||||||
with tempfile.TemporaryDirectory()as J:
|
if error_response:
|
||||||
|
return error_response
|
||||||
try:
|
try:
|
||||||
K=git.Repo.clone_from(self.repo_path(F,C),J);E=[]
|
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:
|
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()})
|
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
||||||
except git.GitCommandError as D:
|
commits = []
|
||||||
if'unknown revision or path'in str(D):E=[]
|
try:
|
||||||
else:raise
|
for commit in list(temp_repo.iter_commits(branch, max_count=limit)):
|
||||||
return web.json_response({_H:C,_G:H,'commits':E})
|
commits.append({
|
||||||
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)
|
"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
|
@require_auth
|
||||||
async def file_content(self, request):
|
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)
|
username = request['username']
|
||||||
if G:return G
|
repo_name = request.match_info['repo_name']
|
||||||
with tempfile.TemporaryDirectory()as H:
|
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:
|
try:
|
||||||
J=git.Repo.clone_from(self.repo_path(F,B),H)
|
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
||||||
try:J.git.checkout(E)
|
try:
|
||||||
except git.GitCommandError:return web.Response(text=f"Branch '{E}' not found",status=404)
|
temp_repo.git.checkout(branch)
|
||||||
D=os.path.join(H,C)
|
except git.GitCommandError:
|
||||||
if not os.path.exists(D):return web.Response(text=f"File '{C}' not found",status=404)
|
return web.Response(text=f"Branch '{branch}' 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})
|
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:
|
else:
|
||||||
try:
|
try:
|
||||||
with open(D,'r')as L:M=L.read()
|
with open(file_full_path, 'r') as f:
|
||||||
return web.Response(text=M)
|
content = f.read()
|
||||||
except UnicodeDecodeError:return web.Response(text=f"Cannot display binary file content for '{C}'",status=400)
|
return web.Response(text=content)
|
||||||
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)
|
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
|
@require_auth
|
||||||
async def git_smart_http(self, request):
|
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
|
username = request['username']
|
||||||
async def K():
|
repository_path = request['repository_path']
|
||||||
B=C.lstrip('/')
|
path = request.path
|
||||||
if B.endswith(F):A=B[:-len(F)]
|
async def get_repository_path():
|
||||||
elif B.endswith(E):A=B[:-len(E)]
|
req_path = path.lstrip('/')
|
||||||
elif B.endswith(D):A=B[:-len(D)]
|
if req_path.endswith('/info/refs'):
|
||||||
else:A=B
|
repo_name = req_path[:-len('/info/refs')]
|
||||||
if A.endswith(_F):A=A[:-4]
|
elif req_path.endswith('/git-upload-pack'):
|
||||||
A=A[4:];G=N.joinpath(A+_F);logger.info(f"Resolved repo path: {G}");return G
|
repo_name = req_path[:-len('/git-upload-pack')]
|
||||||
async def O(service):
|
elif req_path.endswith('/git-receive-pack'):
|
||||||
C=service;D=await K();logger.info(f"handle_info_refs: {D}")
|
repo_name = req_path[:-len('/git-receive-pack')]
|
||||||
if not os.path.exists(D):return web.Response(text=_J,status=404)
|
else:
|
||||||
L=[C,J,'--advertise-refs',str(D)]
|
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:
|
try:
|
||||||
E=await asyncio.create_subprocess_exec(*L,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.PIPE);M,F=await E.communicate()
|
process = await asyncio.create_subprocess_exec(
|
||||||
if E.returncode!=0:logger.error(f"Git command failed: {F.decode()}");return web.Response(text=f"Git error: {F.decode()}",status=500)
|
*cmd,
|
||||||
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
|
stdout=asyncio.subprocess.PIPE,
|
||||||
except Exception as H:logger.error(f"Error handling info/refs: {str(H)}");return web.Response(text=f"Server error: {str(H)}",status=500)
|
stderr=asyncio.subprocess.PIPE
|
||||||
async def L(service):
|
)
|
||||||
B=service;C=await K();logger.info(f"handle_service_rpc: {C}")
|
stdout, stderr = await process.communicate()
|
||||||
if not os.path.exists(C):return web.Response(text=_J,status=404)
|
if process.returncode != 0:
|
||||||
if not A.headers.get(I)==f"application/x-{B}-request":return web.Response(text='Invalid Content-Type',status=403)
|
logger.error(f"Git command failed: {stderr.decode()}")
|
||||||
G=await A.read();H=[B,J,str(C)]
|
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:
|
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)
|
process = await asyncio.create_subprocess_exec(
|
||||||
if D.returncode!=0:logger.error(f"Git command failed: {E.decode()}");return web.Response(text=f"Git error: {E.decode()}",status=500)
|
*cmd,
|
||||||
return web.Response(body=L,content_type=f"application/x-{B}-result")
|
stdin=asyncio.subprocess.PIPE,
|
||||||
except Exception as F:logger.error(f"Error handling service RPC: {str(F)}");return web.Response(text=f"Server error: {str(F)}",status=500)
|
stdout=asyncio.subprocess.PIPE,
|
||||||
if A.method=='GET'and C.endswith(F):
|
stderr=asyncio.subprocess.PIPE
|
||||||
M=A.query.get('service')
|
)
|
||||||
if M in(H,G):return await O(M)
|
stdout, stderr = await process.communicate(input=body)
|
||||||
else:return web.Response(text='Smart HTTP requires service parameter',status=400)
|
if process.returncode != 0:
|
||||||
elif A.method==B and E in C:return await L(H)
|
logger.error(f"Git command failed: {stderr.decode()}")
|
||||||
elif A.method==B and D in C:return await L(G)
|
return web.Response(text=f"Git error: {stderr.decode()}", status=500)
|
||||||
return web.Response(text='Not found',status=404)
|
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__':
|
if __name__ == '__main__':
|
||||||
try:import uvloop;asyncio.set_event_loop_policy(uvloop.EventLoopPolicy());logger.info('Using uvloop for improved performance')
|
try:
|
||||||
except ImportError:logger.info('uvloop not available, using standard event loop')
|
import uvloop
|
||||||
app=GitApplication();logger.info('Starting Git server on port 8080');web.run_app(app,port=8080)
|
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)
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import aiohttp
|
||||||
|
|
||||||
|
ENABLED = False
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
import dataset
|
||||||
|
from sqlalchemy import event
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
queue = asyncio.Queue()
|
||||||
|
|
||||||
|
class State:
|
||||||
|
do_not_sync = False
|
||||||
|
|
||||||
|
async def sync_service(app):
|
||||||
|
if not ENABLED:
|
||||||
|
return
|
||||||
|
session = aiohttp.ClientSession()
|
||||||
|
async with session.ws_connect('http://localhost:3131/ws') as ws:
|
||||||
|
async def receive():
|
||||||
|
|
||||||
|
queries_synced = 0
|
||||||
|
async for msg in ws:
|
||||||
|
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||||
|
try:
|
||||||
|
data = json.loads(msg.data)
|
||||||
|
State.do_not_sync = True
|
||||||
|
app.db.execute(*data)
|
||||||
|
app.db.commit()
|
||||||
|
State.do_not_sync = False
|
||||||
|
queries_synced += 1
|
||||||
|
print("queries synced: " + str(queries_synced))
|
||||||
|
print(*data)
|
||||||
|
await app.services.socket.broadcast_event()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
pass
|
||||||
|
#print(f"Received: {msg.data}")
|
||||||
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
|
break
|
||||||
|
async def write():
|
||||||
|
while True:
|
||||||
|
msg = await queue.get()
|
||||||
|
await ws.send_str(json.dumps(msg,default=str))
|
||||||
|
queue.task_done()
|
||||||
|
|
||||||
|
await asyncio.gather(receive(), write())
|
||||||
|
|
||||||
|
await session.close()
|
||||||
|
|
||||||
|
queries_queued = 0
|
||||||
|
# Attach a listener to log all executed statements
|
||||||
|
@event.listens_for(Engine, "before_cursor_execute")
|
||||||
|
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
|
||||||
|
if not ENABLED:
|
||||||
|
return
|
||||||
|
global queries_queued
|
||||||
|
if State.do_not_sync:
|
||||||
|
print(statement,parameters)
|
||||||
|
return
|
||||||
|
if statement.startswith("SELECT"):
|
||||||
|
return
|
||||||
|
queue.put_nowait((statement, parameters))
|
||||||
|
queries_queued += 1
|
||||||
|
print("Queries queued: " + str(queries_queued))
|
||||||
|
|
||||||
|
async def websocket_handler(request):
|
||||||
|
queries_broadcasted = 0
|
||||||
|
ws = web.WebSocketResponse()
|
||||||
|
await ws.prepare(request)
|
||||||
|
request.app['websockets'].append(ws)
|
||||||
|
async for msg in ws:
|
||||||
|
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||||
|
for client in request.app['websockets']:
|
||||||
|
if client != ws:
|
||||||
|
await client.send_str(msg.data)
|
||||||
|
cursor = request.app['db'].cursor()
|
||||||
|
data = json.loads(msg.data)
|
||||||
|
queries_broadcasted += 1
|
||||||
|
|
||||||
|
cursor.execute(*data)
|
||||||
|
cursor.close()
|
||||||
|
print("Queries broadcasted: " + str(queries_broadcasted))
|
||||||
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
|
print(f'WebSocket connection closed with exception {ws.exception()}')
|
||||||
|
|
||||||
|
request.app['websockets'].remove(ws)
|
||||||
|
return ws
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
app['websockets'] = []
|
||||||
|
|
||||||
|
app.router.add_get('/ws', websocket_handler)
|
||||||
|
|
||||||
|
async def on_startup(app):
|
||||||
|
app['db'] = sqlite3.connect('snek.db')
|
||||||
|
print("Server starting...")
|
||||||
|
|
||||||
|
async def on_cleanup(app):
|
||||||
|
for ws in app['websockets']:
|
||||||
|
await ws.close()
|
||||||
|
app['db'].close()
|
||||||
|
|
||||||
|
app.on_startup.append(on_startup)
|
||||||
|
app.on_cleanup.append(on_cleanup)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
web.run_app(app, host='127.0.0.1', port=3131)
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import asyncssh
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
global _app
|
||||||
|
|
||||||
|
def set_app(app):
|
||||||
|
global _app
|
||||||
|
_app = app
|
||||||
|
|
||||||
|
def get_app():
|
||||||
|
return _app
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
roots = {}
|
||||||
|
|
||||||
|
class SFTPServer(asyncssh.SFTPServer):
|
||||||
|
|
||||||
|
def __init__(self, chan: asyncssh.SSHServerChannel):
|
||||||
|
self.root = get_app().services.user.get_home_folder_by_username(
|
||||||
|
chan.get_extra_info('username')
|
||||||
|
)
|
||||||
|
self.root.mkdir(exist_ok=True)
|
||||||
|
self.root = str(self.root)
|
||||||
|
super().__init__(chan, chroot=self.root)
|
||||||
|
|
||||||
|
def map_path(self, path):
|
||||||
|
mapped_path = Path(self.root).joinpath(path.lstrip(b"/").decode())
|
||||||
|
logger.debug(f"Mapping client path {path} to {mapped_path}")
|
||||||
|
return str(mapped_path).encode()
|
||||||
|
|
||||||
|
class SSHServer(asyncssh.SSHServer):
|
||||||
|
def password_auth_supported(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def validate_password(self, username, password):
|
||||||
|
logger.debug(f"Validating credentials for user {username}")
|
||||||
|
result = get_app().services.user.authenticate_sync(username,password)
|
||||||
|
logger.info(f"Validating credentials for user {username}: {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def start_ssh_server(app,host,port):
|
||||||
|
set_app(app)
|
||||||
|
logger.info("Starting SFTP server setup")
|
||||||
|
|
||||||
|
host_key_path = Path("drive") / ".ssh" / "sftp_server_key"
|
||||||
|
host_key_path.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
try:
|
||||||
|
if not host_key_path.exists():
|
||||||
|
logger.info(f"Generating new host key at {host_key_path}")
|
||||||
|
key = asyncssh.generate_private_key("ecdsa-sha2-nistp256")
|
||||||
|
key.write_private_key(host_key_path)
|
||||||
|
else:
|
||||||
|
logger.info(f"Loading existing host key from {host_key_path}")
|
||||||
|
key = asyncssh.read_private_key(host_key_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to generate or load host key: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.info(f"Starting SFTP server on 127.0.0.1:{port}")
|
||||||
|
try:
|
||||||
|
x = await asyncssh.listen(
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
#process_factory=handle_client,
|
||||||
|
server_host_keys=[key],
|
||||||
|
server_factory=SSHServer,
|
||||||
|
sftp_factory=SFTPServer
|
||||||
|
)
|
||||||
|
return x
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to start SFTP server. Already running.")
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
+69
-37
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
// MIT License
|
// MIT License
|
||||||
|
|
||||||
import { Schedule } from './schedule.js';
|
import { Schedule } from "./schedule.js";
|
||||||
import { EventHandler } from "./event-handler.js";
|
import { EventHandler } from "./event-handler.js";
|
||||||
import { Socket } from "./socket.js";
|
import { Socket } from "./socket.js";
|
||||||
|
|
||||||
@@ -16,11 +16,11 @@ export class RESTClient {
|
|||||||
|
|
||||||
async get(url, params = {}) {
|
async get(url, params = {}) {
|
||||||
const encodedParams = new URLSearchParams(params);
|
const encodedParams = new URLSearchParams(params);
|
||||||
if (encodedParams) url += '?' + encodedParams;
|
if (encodedParams) url += "?" + encodedParams;
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -32,9 +32,9 @@ export class RESTClient {
|
|||||||
|
|
||||||
async post(url, data) {
|
async post(url, data) {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
@@ -50,7 +50,10 @@ export class RESTClient {
|
|||||||
export class Chat extends EventHandler {
|
export class Chat extends EventHandler {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this._url = window.location.hostname === 'localhost' ? 'ws://localhost/chat.ws' : 'wss://' + window.location.hostname + '/chat.ws';
|
this._url =
|
||||||
|
window.location.hostname === "localhost"
|
||||||
|
? "ws://localhost/chat.ws"
|
||||||
|
: "wss://" + window.location.hostname + "/chat.ws";
|
||||||
this._socket = null;
|
this._socket = null;
|
||||||
this._waitConnect = null;
|
this._waitConnect = null;
|
||||||
this._promises = {};
|
this._promises = {};
|
||||||
@@ -81,7 +84,7 @@ export class Chat extends EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generateUniqueId() {
|
generateUniqueId() {
|
||||||
return 'id-' + Math.random().toString(36).substr(2, 9);
|
return "id-" + Math.random().toString(36).substr(2, 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
call(method, ...args) {
|
call(method, ...args) {
|
||||||
@@ -109,7 +112,7 @@ export class Chat extends EventHandler {
|
|||||||
this._socket.onclose = () => {
|
this._socket.onclose = () => {
|
||||||
this._waitSocket = null;
|
this._waitSocket = null;
|
||||||
this._socket = null;
|
this._socket = null;
|
||||||
this.emit('close');
|
this.emit("close");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,19 +130,21 @@ export class NotificationAudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sounds = {
|
sounds = {
|
||||||
"message": "/audio/soundfx.d_beep3.mp3",
|
message: "/audio/soundfx.d_beep3.mp3",
|
||||||
"mention": "/audio/750607__deadrobotmusic__notification-sound-1.wav",
|
mention: "/audio/750607__deadrobotmusic__notification-sound-1.wav",
|
||||||
"messageOtherChannel": "/audio/750608__deadrobotmusic__notification-sound-2.wav",
|
messageOtherChannel:
|
||||||
"ping": "/audio/750609__deadrobotmusic__notification-sound-3.wav",
|
"/audio/750608__deadrobotmusic__notification-sound-2.wav",
|
||||||
}
|
ping: "/audio/750609__deadrobotmusic__notification-sound-3.wav",
|
||||||
|
};
|
||||||
|
|
||||||
play(soundIndex = 0) {
|
play(soundIndex = 0) {
|
||||||
this.schedule.delay(() => {
|
this.schedule.delay(() => {
|
||||||
new Audio(this.sounds[soundIndex]).play()
|
new Audio(this.sounds[soundIndex])
|
||||||
|
.play()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.debug("Gave sound notification");
|
console.debug("Gave sound notification");
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error("Notification failed:", error);
|
console.error("Notification failed:", error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -152,38 +157,61 @@ export class App extends EventHandler {
|
|||||||
rpc = null;
|
rpc = null;
|
||||||
audio = null;
|
audio = null;
|
||||||
user = {};
|
user = {};
|
||||||
|
typeLock = null;
|
||||||
|
typeListener = null;
|
||||||
|
typeEventChannelUid = null;
|
||||||
|
_debug = false
|
||||||
|
async set_typing(channel_uid) {
|
||||||
|
this.typeEventChannel_uid = channel_uid;
|
||||||
|
}
|
||||||
|
debug() {
|
||||||
|
this._debug = !this._debug;
|
||||||
|
this.ws._debug = this._debug;
|
||||||
|
}
|
||||||
async ping(...args) {
|
async ping(...args) {
|
||||||
if (this.is_pinging) return false
|
if (this.is_pinging) return false;
|
||||||
this.is_pinging = true
|
this.is_pinging = true;
|
||||||
await this.rpc.ping(...args);
|
await this.rpc.ping(...args);
|
||||||
this.is_pinging = false
|
this.is_pinging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async forcePing(...arg) {
|
async forcePing(...arg) {
|
||||||
await this.rpc.ping(...args);
|
await this.rpc.ping(...args);
|
||||||
}
|
}
|
||||||
|
starField = null
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.ws = new Socket();
|
this.ws = new Socket();
|
||||||
this.rpc = this.ws.client;
|
this.rpc = this.ws.client;
|
||||||
this.audio = new NotificationAudio(500);
|
this.audio = new NotificationAudio(500);
|
||||||
this.is_pinging = false
|
this.is_pinging = false;
|
||||||
this.ping_interval = setInterval(() => {
|
this.ping_interval = setInterval(() => {
|
||||||
this.ping("active")
|
this.ping("active");
|
||||||
}, 15000)
|
}, 15000);
|
||||||
|
this.typeEventChannelUid = null;
|
||||||
|
this.typeListener = setInterval(() => {
|
||||||
|
if (this.typeEventChannelUid) {
|
||||||
|
this.rpc.set_typing(this.typeEventChannelUid);
|
||||||
|
this.typeEventChannelUid = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const me = this;
|
||||||
const me = this
|
|
||||||
this.ws.addEventListener("connected", (data) => {
|
this.ws.addEventListener("connected", (data) => {
|
||||||
this.ping("online")
|
this.ping("online");
|
||||||
|
});
|
||||||
|
this.ws.addEventListener("reconnecting", (data) => {
|
||||||
|
this.starField?.showNotify("Connecting..","#CC0000")
|
||||||
})
|
})
|
||||||
this.ws.addEventListener("channel-message", (data) => {
|
this.ws.addEventListener("channel-message", (data) => {
|
||||||
me.emit("channel-message", data);
|
me.emit("channel-message", data);
|
||||||
});
|
});
|
||||||
|
this.ws.addEventListener("data", (data) => {
|
||||||
this.rpc.getUser(null).then(user => {
|
if(this._debug){
|
||||||
|
console.debug(data)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.rpc.getUser(null).then((user) => {
|
||||||
me.user = user;
|
me.user = user;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -203,31 +231,35 @@ export class App extends EventHandler {
|
|||||||
timeAgo(date1, date2) {
|
timeAgo(date1, date2) {
|
||||||
const diffMs = Math.abs(date2 - date1);
|
const diffMs = Math.abs(date2 - date1);
|
||||||
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
const hours = Math.floor(
|
||||||
|
(diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60),
|
||||||
|
);
|
||||||
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
if (days) {
|
if (days) {
|
||||||
return `${days} ${days > 1 ? 'days' : 'day'} ago`;
|
return `${days} ${days > 1 ? "days" : "day"} ago`;
|
||||||
}
|
}
|
||||||
if (hours) {
|
if (hours) {
|
||||||
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`;
|
return `${hours} ${hours > 1 ? "hours" : "hour"} ago`;
|
||||||
}
|
}
|
||||||
if (minutes) {
|
if (minutes) {
|
||||||
return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`;
|
return `${minutes} ${minutes > 1 ? "minutes" : "minute"} ago`;
|
||||||
}
|
}
|
||||||
return 'just now';
|
return "just now";
|
||||||
}
|
}
|
||||||
|
|
||||||
async benchMark(times = 100, message = "Benchmark Message") {
|
async benchMark(times = 100, message = "Benchmark Message") {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
const me = this;
|
const me = this;
|
||||||
for (let i = 0; i < times; i++) {
|
for (let i = 0; i < times; i++) {
|
||||||
promises.push(this.rpc.getChannels().then(channels => {
|
promises.push(
|
||||||
channels.forEach(channel => {
|
this.rpc.getChannels().then((channels) => {
|
||||||
|
channels.forEach((channel) => {
|
||||||
me.rpc.sendMessage(channel.uid, `${message} ${i}`);
|
me.rpc.sendMessage(channel.uid, `${message} ${i}`);
|
||||||
});
|
});
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+194
-18
@@ -5,6 +5,10 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.gallery {
|
.gallery {
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -25,24 +29,26 @@ body {
|
|||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
color: #e6e6e6;
|
color: #e6e6e6;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-columns: auto 1fr;
|
||||||
height: 100vh;
|
grid-template-rows: auto 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"header header"
|
||||||
|
"sidebar chat-area";
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
grid-area: chat-area;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background-color: #000000;
|
grid-area: header;
|
||||||
padding-top: 10px;
|
padding: 10px 20px;
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -95,13 +101,11 @@ h2 {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #000000;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background-color: #000000;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +136,11 @@ footer {
|
|||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
background: #000000;
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@@ -152,6 +160,10 @@ footer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-messages picture img {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-messages::-webkit-scrollbar {
|
.chat-messages::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -228,15 +240,14 @@ footer {
|
|||||||
|
|
||||||
.chat-input {
|
.chat-input {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
background-color: #000000;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"], .chat-input textarea {
|
input[type="text"], .chat-input textarea {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: #000000;
|
|
||||||
color: white;
|
color: white;
|
||||||
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -273,13 +284,18 @@ input[type="text"], .chat-input textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.author, .time {
|
.author {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(:hover, :focus-within, :active) .time {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.switch-user {
|
.message.switch-user {
|
||||||
@@ -291,6 +307,7 @@ input[type="text"], .chat-input textarea {
|
|||||||
.avatar {
|
.avatar {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
max-height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.author {
|
.author {
|
||||||
@@ -298,9 +315,10 @@ input[type="text"], .chat-input textarea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message:has(+ .message.switch-user), .message:last-child {
|
.message:has(+ .message.switch-user), .message:has(+ .message.long-time), .message:not(:has(+ .message)) {
|
||||||
.time {
|
.time {
|
||||||
display: block;
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,11 +352,11 @@ a {
|
|||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
background-color: #000000;
|
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
grid-area: sidebar;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar h2 {
|
.sidebar h2 {
|
||||||
@@ -366,6 +384,23 @@ a {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes glow {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 5px #3498db;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 20px #3498db, 0 0 30px #3498db;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 5px #3498db;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow {
|
||||||
|
animation: glow 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
@media only screen and (max-width: 768px) {
|
||||||
|
|
||||||
header {
|
header {
|
||||||
@@ -375,25 +410,29 @@ a {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display: block;
|
display: block;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
body {
|
body {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
@@ -409,3 +448,140 @@ a {
|
|||||||
position:sticky;
|
position:sticky;
|
||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #000; /* Deep black */
|
||||||
|
color: #f1f1f1;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.8);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 400px;
|
||||||
|
|
||||||
|
animation: dialogFadeIn 0.3s ease-out, dialogScaleIn 0.3s ease-out;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Backdrop styling */
|
||||||
|
dialog::backdrop {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title and content */
|
||||||
|
dialog .dialog-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog .dialog-content {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #ccc;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button layout */
|
||||||
|
dialog .dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog .dialog-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes dialogFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialogScaleIn {
|
||||||
|
from {
|
||||||
|
transform: scale(0.95) translate(-50%, -50%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1) translate(-50%, -50%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog .dialog-button.primary {
|
||||||
|
background-color: #f05a28;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog .dialog-button.primary:hover {
|
||||||
|
background-color: #f05a28;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog .dialog-button.secondary {
|
||||||
|
background-color: #f0a328;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog .dialog-button.secondary:hover {
|
||||||
|
background-color: #f0b84c;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.embed-url-link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-url-link img,
|
||||||
|
.embed-url-link video,
|
||||||
|
.embed-url-link iframe,
|
||||||
|
.embed-url-link div {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 400px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-url-link .page-site {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #aaa;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-url-link .page-name {
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #f05a28;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-url-link .page-description {
|
||||||
|
font-size: 1em;
|
||||||
|
color: #e6e6e6;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-url-link .page-link {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #f05a28;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
+356
-52
@@ -1,69 +1,373 @@
|
|||||||
// Written by retoor@molodetz.nl
|
import { app } from "../app.js";
|
||||||
|
|
||||||
// This JavaScript class defines a custom HTML element for a chat input widget, featuring a text area and an upload button. It handles user input and triggers events for input changes and message submission.
|
class ChatInputComponent extends HTMLElement {
|
||||||
|
autoCompletions = {
|
||||||
// Includes standard DOM manipulation methods; no external imports used.
|
"example 1": () => {},
|
||||||
|
"example 2": () => {},
|
||||||
// MIT License: This code is open-source and can be reused and distributed under the terms of the MIT License.
|
}
|
||||||
|
hiddenCompletions = {
|
||||||
class ChatInputElement extends HTMLElement {
|
"/starsRender": () => {
|
||||||
_chatWindow = null
|
app.rpc.starsRender(this.channelUid,this.value.replace("/starsRender ",""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
users = []
|
||||||
|
textarea = null
|
||||||
|
_value = ""
|
||||||
|
lastUpdateEvent = null
|
||||||
|
previousValue = ""
|
||||||
|
lastChange = null
|
||||||
|
changed = false
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.lastUpdateEvent = new Date();
|
||||||
this.component = document.createElement('div');
|
this.textarea = document.createElement("textarea");
|
||||||
this.shadowRoot.appendChild(this.component);
|
this.value = this.getAttribute("value") || "";
|
||||||
|
this.previousValue = this.value;
|
||||||
|
this.lastChange = new Date();
|
||||||
|
this.changed = false;
|
||||||
}
|
}
|
||||||
set chatWindow(value){
|
|
||||||
this._chatWindow = value
|
get value() {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(value) {
|
||||||
|
this._value = value || "";
|
||||||
|
this.textarea.value = this._value;
|
||||||
|
}
|
||||||
|
get allAutoCompletions() {
|
||||||
|
return Object.assign({},this.autoCompletions,this.hiddenCompletions)
|
||||||
|
}
|
||||||
|
resolveAutoComplete() {
|
||||||
|
let count = 0;
|
||||||
|
let value = null;
|
||||||
|
|
||||||
|
Object.keys(this.allAutoCompletions).forEach((key) => {
|
||||||
|
if (key.startsWith(this.value.split(" ")[0])) {
|
||||||
|
count++;
|
||||||
|
value = key;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (count == 1) return value;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive() {
|
||||||
|
return document.activeElement === this.textarea;
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.textarea.focus();
|
||||||
|
}
|
||||||
|
getAuthors(){
|
||||||
|
let authors = []
|
||||||
|
for (let i = 0; i < this.users.length; i++) {
|
||||||
|
authors.push(this.users[i].username)
|
||||||
|
authors.push(this.users[i].nick)
|
||||||
|
}
|
||||||
|
return authors
|
||||||
|
|
||||||
|
}
|
||||||
|
extractMentions(text) {
|
||||||
|
const regex = /@([a-zA-Z0-9_-]+)/g;
|
||||||
|
const mentions = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
mentions.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mentions;
|
||||||
|
}
|
||||||
|
matchMentionsToAuthors(mentions, authors) {
|
||||||
|
return mentions.map(mention => {
|
||||||
|
let closestAuthor = null;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
const lowerMention = mention.toLowerCase();
|
||||||
|
|
||||||
|
authors.forEach(author => {
|
||||||
|
const lowerAuthor = author.toLowerCase();
|
||||||
|
let distance = this.levenshteinDistance(lowerMention, lowerAuthor);
|
||||||
|
|
||||||
|
|
||||||
|
if(!this.isSubsequence(lowerMention,lowerAuthor)) {
|
||||||
|
distance += 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
closestAuthor = author;
|
||||||
|
|
||||||
}
|
}
|
||||||
get chatWindow(){
|
|
||||||
return this._chatWindow
|
|
||||||
}
|
|
||||||
get channelUid() {
|
|
||||||
return this.chatWindow.channel.uid
|
|
||||||
}
|
|
||||||
connectedCallback() {
|
|
||||||
const link = document.createElement('link');
|
|
||||||
link.rel = 'stylesheet';
|
|
||||||
link.href = '/base.css';
|
|
||||||
this.component.appendChild(link);
|
|
||||||
|
|
||||||
this.container = document.createElement('div');
|
|
||||||
this.container.classList.add('chat-input');
|
|
||||||
this.container.innerHTML = `
|
|
||||||
<textarea placeholder="Type a message..." rows="2"></textarea>
|
|
||||||
<upload-button></upload-button>
|
|
||||||
`;
|
|
||||||
this.textBox = this.container.querySelector('textarea');
|
|
||||||
this.uploadButton = this.container.querySelector('upload-button');
|
|
||||||
this.uploadButton.chatInput = this
|
|
||||||
this.textBox.addEventListener('input', (e) => {
|
|
||||||
this.dispatchEvent(new CustomEvent('input', { detail: e.target.value, bubbles: true }));
|
|
||||||
const message = e.target.value;
|
|
||||||
const button = this.container.querySelector('button');
|
|
||||||
button.disabled = !message;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.textBox.addEventListener('change', (e) => {
|
return { mention, closestAuthor, distance: minDistance };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
levenshteinDistance(a, b) {
|
||||||
|
const matrix = [];
|
||||||
|
|
||||||
|
// Initialize the first row and column
|
||||||
|
for (let i = 0; i <= b.length; i++) {
|
||||||
|
matrix[i] = [i];
|
||||||
|
}
|
||||||
|
for (let j = 0; j <= a.length; j++) {
|
||||||
|
matrix[0][j] = j;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in the matrix
|
||||||
|
for (let i = 1; i <= b.length; i++) {
|
||||||
|
for (let j = 1; j <= a.length; j++) {
|
||||||
|
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
||||||
|
matrix[i][j] = matrix[i - 1][j - 1];
|
||||||
|
} else {
|
||||||
|
matrix[i][j] = Math.min(
|
||||||
|
matrix[i - 1][j] + 1, // Deletion
|
||||||
|
matrix[i][j - 1] + 1, // Insertion
|
||||||
|
matrix[i - 1][j - 1] + 1 // Substitution
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix[b.length][a.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
replaceMentionsWithAuthors(text) {
|
||||||
|
const authors = this.getAuthors();
|
||||||
|
const mentions = this.extractMentions(text);
|
||||||
|
|
||||||
|
const matches = this.matchMentionsToAuthors(mentions, authors);
|
||||||
|
let updatedText = text;
|
||||||
|
matches.forEach(({ mention, closestAuthor }) => {
|
||||||
|
const mentionRegex = new RegExp(`@${mention}`, 'g');
|
||||||
|
updatedText = updatedText.replace(mentionRegex, `@${closestAuthor}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
this.user = null
|
||||||
|
app.rpc.getUser(null).then((user) => {
|
||||||
|
this.user=user
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const me = this;
|
||||||
|
this.liveType = this.getAttribute("live-type") === "true";
|
||||||
|
this.liveTypeInterval =
|
||||||
|
parseInt(this.getAttribute("live-type-interval")) || 6;
|
||||||
|
this.channelUid = this.getAttribute("channel");
|
||||||
|
|
||||||
|
app.rpc.getRecentUsers(this.channelUid).then(users=>{
|
||||||
|
this.users = users
|
||||||
|
})
|
||||||
|
this.messageUid = null;
|
||||||
|
|
||||||
|
this.classList.add("chat-input");
|
||||||
|
|
||||||
|
this.textarea.setAttribute("placeholder", "Type a message...");
|
||||||
|
this.textarea.setAttribute("rows", "2");
|
||||||
|
|
||||||
|
this.appendChild(this.textarea);
|
||||||
|
|
||||||
|
this.uploadButton = document.createElement("upload-button");
|
||||||
|
this.uploadButton.setAttribute("channel", this.channelUid);
|
||||||
|
this.uploadButton.addEventListener("upload", (e) => {
|
||||||
|
this.dispatchEvent(new CustomEvent("upload", e));
|
||||||
|
});
|
||||||
|
this.uploadButton.addEventListener("uploaded", (e) => {
|
||||||
|
this.dispatchEvent(new CustomEvent("uploaded", e));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.appendChild(this.uploadButton);
|
||||||
|
|
||||||
|
this.textarea.addEventListener("keyup", (e) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
this.value = "";
|
||||||
|
e.target.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.value = e.target.value;
|
||||||
|
this.changed = true;
|
||||||
|
this.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.textarea.addEventListener("keydown", (e) => {
|
||||||
|
this.value = e.target.value;
|
||||||
|
let autoCompletion = null;
|
||||||
|
if (e.key === "Tab") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));
|
autoCompletion = this.resolveAutoComplete();
|
||||||
console.error(e.target.value);
|
if (autoCompletion) {
|
||||||
});
|
e.target.value = autoCompletion;
|
||||||
|
this.value = autoCompletion;
|
||||||
this.textBox.addEventListener('keydown', (e) => {
|
return;
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
}
|
||||||
|
}
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const message = e.target.value.trim();
|
|
||||||
if (!message) return;
|
const message = me.replaceMentionsWithAuthors(this.value);
|
||||||
this.dispatchEvent(new CustomEvent('submit', { detail: message, bubbles: true }));
|
e.target.value = "";
|
||||||
e.target.value = '';
|
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let autoCompletionHandler = this.allAutoCompletions[this.value.split(" ")[0]];
|
||||||
|
if (autoCompletionHandler) {
|
||||||
|
autoCompletionHandler();
|
||||||
|
this.value = "";
|
||||||
|
this.previousValue = "";
|
||||||
|
e.target.value = "";
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateMessage()
|
||||||
|
app.rpc.finalizeMessage(this.messageUid)
|
||||||
|
this.value = "";
|
||||||
|
this.previousValue = "";
|
||||||
|
this.messageUid = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.component.appendChild(this.container);
|
this.changeInterval = setInterval(() => {
|
||||||
|
if (!this.liveType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.value !== this.previousValue) {
|
||||||
|
if (
|
||||||
|
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
|
||||||
|
this.liveTypeInterval
|
||||||
|
) {
|
||||||
|
this.value = "";
|
||||||
|
this.previousValue = "";
|
||||||
|
}
|
||||||
|
this.lastChange = new Date();
|
||||||
|
}
|
||||||
|
this.update();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
this.addEventListener("upload", (e) => {
|
||||||
|
this.focus();
|
||||||
|
});
|
||||||
|
this.addEventListener("uploaded", function (e) {
|
||||||
|
let message = "";
|
||||||
|
e.detail.files.forEach((file) => {
|
||||||
|
message += `[${file.name}](/channel/attachment/${file.relative_url})`;
|
||||||
|
});
|
||||||
|
app.rpc.sendMessage(this.channelUid, message,true);
|
||||||
|
});
|
||||||
|
setTimeout(()=>{
|
||||||
|
this.focus();
|
||||||
|
},1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
trackSecondsBetweenEvents(event1Time, event2Time) {
|
||||||
|
const millisecondsDifference = event2Time.getTime() - event1Time.getTime();
|
||||||
|
return millisecondsDifference / 1000;
|
||||||
|
}
|
||||||
|
isSubsequence(s, t) {
|
||||||
|
let i = 0, j = 0;
|
||||||
|
while (i < s.length && j < t.length) {
|
||||||
|
if (s[i] === t[j]) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
return i === s.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
newMessage() {
|
||||||
|
if (!this.messageUid) {
|
||||||
|
this.messageUid = "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value = this.replaceMentionsWithAuthors(this.value);
|
||||||
|
this.sendMessage(this.channelUid, this.value,!this.liveType).then((uid) => {
|
||||||
|
if (this.liveType) {
|
||||||
|
this.messageUid = uid;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMessage() {
|
||||||
|
if (this.value[0] == "/") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.messageUid) {
|
||||||
|
this.newMessage();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.messageUid === "?") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof app !== "undefined" &&
|
||||||
|
app.rpc &&
|
||||||
|
typeof app.rpc.updateMessageText === "function"
|
||||||
|
) {
|
||||||
|
app.rpc.updateMessageText(this.messageUid, this.replaceMentionsWithAuthors(this.value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('chat-input', ChatInputElement);
|
updateStatus() {
|
||||||
|
if (this.liveType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) {
|
||||||
|
this.lastUpdateEvent = new Date();
|
||||||
|
if (
|
||||||
|
typeof app !== "undefined" &&
|
||||||
|
app.rpc &&
|
||||||
|
typeof app.rpc.set_typing === "function"
|
||||||
|
) {
|
||||||
|
app.rpc.set_typing(this.channelUid, this.user.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const expired =
|
||||||
|
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
|
||||||
|
this.liveTypeInterval;
|
||||||
|
const changed = this.value !== this.previousValue;
|
||||||
|
|
||||||
|
if (changed || expired) {
|
||||||
|
this.lastChange = new Date();
|
||||||
|
this.updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previousValue = this.value;
|
||||||
|
|
||||||
|
if (this.liveType && expired) {
|
||||||
|
this.value = "";
|
||||||
|
this.previousValue = "";
|
||||||
|
this.messageUid = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
if (this.liveType) {
|
||||||
|
this.updateMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(channelUid, value,is_final) {
|
||||||
|
if (!value.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await app.rpc.sendMessage(channelUid, value,is_final);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("chat-input", ChatInputComponent);
|
||||||
|
|||||||
@@ -13,11 +13,11 @@
|
|||||||
|
|
||||||
class ChatWindowElement extends HTMLElement {
|
class ChatWindowElement extends HTMLElement {
|
||||||
receivedHistory = false;
|
receivedHistory = false;
|
||||||
channel = null
|
channel = null;
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: "open" });
|
||||||
this.component = document.createElement('section');
|
this.component = document.createElement("section");
|
||||||
this.app = app;
|
this.app = app;
|
||||||
this.shadowRoot.appendChild(this.component);
|
this.shadowRoot.appendChild(this.component);
|
||||||
}
|
}
|
||||||
@@ -27,9 +27,9 @@ class ChatWindowElement extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
const link = document.createElement('link');
|
const link = document.createElement("link");
|
||||||
link.rel = 'stylesheet';
|
link.rel = "stylesheet";
|
||||||
link.href = '/base.css';
|
link.href = "/base.css";
|
||||||
this.component.appendChild(link);
|
this.component.appendChild(link);
|
||||||
this.component.classList.add("chat-area");
|
this.component.classList.add("chat-area");
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ class ChatWindowElement extends HTMLElement {
|
|||||||
const chatHeader = document.createElement("div");
|
const chatHeader = document.createElement("div");
|
||||||
chatHeader.classList.add("chat-header");
|
chatHeader.classList.add("chat-header");
|
||||||
|
|
||||||
const chatTitle = document.createElement('h2');
|
const chatTitle = document.createElement("h2");
|
||||||
chatTitle.classList.add("chat-title");
|
chatTitle.classList.add("chat-title");
|
||||||
chatTitle.classList.add("no-select");
|
chatTitle.classList.add("no-select");
|
||||||
chatTitle.innerText = "Loading...";
|
chatTitle.innerText = "Loading...";
|
||||||
@@ -51,11 +51,11 @@ class ChatWindowElement extends HTMLElement {
|
|||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
chatTitle.innerText = channel.name;
|
chatTitle.innerText = channel.name;
|
||||||
|
|
||||||
const channelElement = document.createElement('message-list');
|
const channelElement = document.createElement("message-list");
|
||||||
channelElement.setAttribute("channel", channel.uid);
|
channelElement.setAttribute("channel", channel.uid);
|
||||||
this.container.appendChild(channelElement);
|
this.container.appendChild(channelElement);
|
||||||
|
|
||||||
const chatInput = document.createElement('chat-input');
|
const chatInput = document.createElement("chat-input");
|
||||||
chatInput.chatWindow = this;
|
chatInput.chatWindow = this;
|
||||||
chatInput.addEventListener("submit", (e) => {
|
chatInput.addEventListener("submit", (e) => {
|
||||||
app.rpc.sendMessage(channel.uid, e.detail);
|
app.rpc.sendMessage(channel.uid, e.detail);
|
||||||
@@ -65,8 +65,8 @@ class ChatWindowElement extends HTMLElement {
|
|||||||
this.component.appendChild(this.container);
|
this.component.appendChild(this.container);
|
||||||
|
|
||||||
const messages = await app.rpc.getMessages(channel.uid);
|
const messages = await app.rpc.getMessages(channel.uid);
|
||||||
messages.forEach(message => {
|
messages.forEach((message) => {
|
||||||
if (!message['user_nick']) return;
|
if (!message["user_nick"]) return;
|
||||||
channelElement.addMessage(message);
|
channelElement.addMessage(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -74,9 +74,9 @@ class ChatWindowElement extends HTMLElement {
|
|||||||
channelElement.addEventListener("message", (message) => {
|
channelElement.addEventListener("message", (message) => {
|
||||||
if (me.user.uid !== message.detail.user_uid) app.playSound(0);
|
if (me.user.uid !== message.detail.user_uid) app.playSound(0);
|
||||||
|
|
||||||
message.detail.element.scrollIntoView({"block": "end"});
|
message.detail.element.scrollIntoView({ block: "end" });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('chat-window', ChatWindowElement);
|
customElements.define("chat-window", ChatWindowElement);
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
class DumbTerminal extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: "open" });
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
--terminal-bg: #111;
|
||||||
|
--terminal-fg: #0f0;
|
||||||
|
--terminal-accent: #0ff;
|
||||||
|
--terminal-font: monospace;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
background: var(--terminal-bg);
|
||||||
|
color: var(--terminal-fg);
|
||||||
|
font-family: var(--terminal-font);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-line {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt {
|
||||||
|
color: var(--terminal-accent);
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--terminal-fg);
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-backdrop {
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="output" id="output"></div>
|
||||||
|
<div class="input-line">
|
||||||
|
<span class="prompt">></span>
|
||||||
|
<input type="text" id="input" autocomplete="off" autofocus />
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.outputEl = this.shadowRoot.getElementById("output");
|
||||||
|
this.inputEl = this.shadowRoot.getElementById("input");
|
||||||
|
|
||||||
|
this.history = [];
|
||||||
|
this.historyIndex = -1;
|
||||||
|
|
||||||
|
this.inputEl.addEventListener("keydown", (e) => this.onKeyDown(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(event) {
|
||||||
|
const value = this.inputEl.value;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case "Enter":
|
||||||
|
this.executeCommand(value);
|
||||||
|
this.history.push(value);
|
||||||
|
this.historyIndex = this.history.length;
|
||||||
|
this.inputEl.value = "";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ArrowUp":
|
||||||
|
if (this.historyIndex > 0) {
|
||||||
|
this.historyIndex--;
|
||||||
|
this.inputEl.value = this.history[this.historyIndex];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ArrowDown":
|
||||||
|
if (this.historyIndex < this.history.length - 1) {
|
||||||
|
this.historyIndex++;
|
||||||
|
this.inputEl.value = this.history[this.historyIndex];
|
||||||
|
} else {
|
||||||
|
this.historyIndex = this.history.length;
|
||||||
|
this.inputEl.value = "";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executeCommand(command) {
|
||||||
|
const outputLine = document.createElement("div");
|
||||||
|
outputLine.textContent = `> ${command}`;
|
||||||
|
this.outputEl.appendChild(outputLine);
|
||||||
|
|
||||||
|
const resultLine = document.createElement("div");
|
||||||
|
resultLine.textContent = this.mockExecute(command);
|
||||||
|
this.outputEl.appendChild(resultLine);
|
||||||
|
|
||||||
|
this.outputEl.scrollTop = this.outputEl.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
mockExecute(command) {
|
||||||
|
switch (command.trim()) {
|
||||||
|
case "help":
|
||||||
|
return "Available commands: help, clear, date";
|
||||||
|
case "date":
|
||||||
|
return new Date().toString();
|
||||||
|
case "clear":
|
||||||
|
this.outputEl.innerHTML = "";
|
||||||
|
return "";
|
||||||
|
default:
|
||||||
|
return `Unknown command: ${command}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static method to create a modal dialog with the terminal
|
||||||
|
* @returns {HTMLDialogElement}
|
||||||
|
*/
|
||||||
|
static createModal() {
|
||||||
|
const dialog = document.createElement("dialog");
|
||||||
|
dialog.innerHTML = `
|
||||||
|
<div class="dialog-backdrop">
|
||||||
|
<web-terminal></web-terminal>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(dialog);
|
||||||
|
dialog.showModal();
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("web-terminal", WebTerminal);
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
export class EventHandler {
|
export class EventHandler {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.subscribers = {};
|
this.subscribers = {};
|
||||||
@@ -11,6 +9,7 @@ export class EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emit(type, ...data) {
|
emit(type, ...data) {
|
||||||
if (this.subscribers[type]) this.subscribers[type].forEach(handler => handler(...data));
|
if (this.subscribers[type])
|
||||||
|
this.subscribers[type].forEach((handler) => handler(...data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,25 +2,22 @@
|
|||||||
|
|
||||||
// This JavaScript class defines a custom HTML element <fancy-button>, which creates a styled, clickable button element with customizable size, text, and URL redirect functionality.
|
// This JavaScript class defines a custom HTML element <fancy-button>, which creates a styled, clickable button element with customizable size, text, and URL redirect functionality.
|
||||||
|
|
||||||
|
|
||||||
// MIT License
|
// MIT License
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class FancyButton extends HTMLElement {
|
class FancyButton extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: "open" });
|
||||||
this.url = null;
|
this.url = null;
|
||||||
this.type = "button";
|
this.type = "button";
|
||||||
this.value = null;
|
this.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.container = document.createElement('span');
|
this.container = document.createElement("span");
|
||||||
let size = this.getAttribute('size');
|
let size = this.getAttribute("size");
|
||||||
console.info({ GG: size });
|
console.info({ GG: size });
|
||||||
size = size === 'auto' ? '1%' : '33%';
|
size = size === "auto" ? "1%" : "33%";
|
||||||
|
|
||||||
this.styleElement = document.createElement("style");
|
this.styleElement = document.createElement("style");
|
||||||
this.styleElement.innerHTML = `
|
this.styleElement.innerHTML = `
|
||||||
@@ -50,19 +47,20 @@ class FancyButton extends HTMLElement {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
this.container.appendChild(this.styleElement);
|
this.container.appendChild(this.styleElement);
|
||||||
this.buttonElement = document.createElement('button');
|
this.buttonElement = document.createElement("button");
|
||||||
this.container.appendChild(this.buttonElement);
|
this.container.appendChild(this.buttonElement);
|
||||||
this.shadowRoot.appendChild(this.container);
|
this.shadowRoot.appendChild(this.container);
|
||||||
|
|
||||||
this.url = this.getAttribute('url');
|
this.url = this.getAttribute("url");
|
||||||
|
|
||||||
|
this.value = this.getAttribute("value");
|
||||||
this.value = this.getAttribute('value');
|
this.buttonElement.appendChild(
|
||||||
this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text")));
|
document.createTextNode(this.getAttribute("text")),
|
||||||
|
);
|
||||||
this.buttonElement.addEventListener("click", () => {
|
this.buttonElement.addEventListener("click", () => {
|
||||||
if(this.url == 'submit'){
|
if (this.url == "submit") {
|
||||||
this.closest('form').submit()
|
this.closest("form").submit();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.url === "/back" || this.url === "/back/") {
|
if (this.url === "/back" || this.url === "/back/") {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
.file-manager {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #111;
|
||||||
|
color: #ddd;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.file-tile {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.file-tile:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
.file-icon {
|
||||||
|
font-size: 40px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.file-name {
|
||||||
|
font-size: 14px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
.file-tile img {
|
||||||
|
max-width: 80%;
|
||||||
|
height: auto;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/* AΒ <file-browser> custom element that talks toΒ /api/files */
|
||||||
|
class FileBrowser extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: "open" });
|
||||||
|
this.path = ""; // current virtual path ("" = ROOT)
|
||||||
|
this.offset = 0; // pagination offset
|
||||||
|
this.limit = 40; // items per request
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.path = this.getAttribute("path") || "";
|
||||||
|
this.renderShell();
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- UI scaffolding -------------------------------------------
|
||||||
|
renderShell() {
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host { display:block; font-family: system-ui, sans-serif; box-sizing: border-box; }
|
||||||
|
nav { display:flex; flex-wrap:wrap; gap:.5rem; margin:.5rem 0; align-items:center; }
|
||||||
|
button { padding:.35rem .65rem; border:none; border-radius:4px; background:#f05a28; color:#fff; cursor:pointer; font:inherit; }
|
||||||
|
button:disabled { background:#999; cursor:not-allowed; }
|
||||||
|
.crumb { font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||||
|
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:1rem; }
|
||||||
|
.tile { border:1px solid #f05a28; border-radius:8px; padding:.5rem; background:#000000; text-align:center; cursor:pointer; transition:box-shadow .2s ease; }
|
||||||
|
.tile:hover { box-shadow:0 2px 8px rgba(0,0,0,.1); }
|
||||||
|
img.thumb { width:100%; height:90px; object-fit:cover; border-radius:6px; }
|
||||||
|
.icon { font-size:48px; line-height:90px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<button id="up">β¬
οΈΒ Up</button>
|
||||||
|
<span class="crumb" id="crumb"></span>
|
||||||
|
</nav>
|
||||||
|
<div class="grid" id="grid"></div>
|
||||||
|
<nav>
|
||||||
|
<button id="prev">Prev</button>
|
||||||
|
<button id="next">Next</button>
|
||||||
|
</nav>
|
||||||
|
`;
|
||||||
|
this.shadowRoot
|
||||||
|
.getElementById("up")
|
||||||
|
.addEventListener("click", () => this.goUp());
|
||||||
|
this.shadowRoot.getElementById("prev").addEventListener("click", () => {
|
||||||
|
if (this.offset > 0) {
|
||||||
|
this.offset -= this.limit;
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.shadowRoot.getElementById("next").addEventListener("click", () => {
|
||||||
|
this.offset += this.limit;
|
||||||
|
this.load();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Networking ----------------------------------------------
|
||||||
|
async load() {
|
||||||
|
const r = await fetch(
|
||||||
|
`/drive.json?path=${encodeURIComponent(this.path)}&offset=${this.offset}&limit=${this.limit}`,
|
||||||
|
);
|
||||||
|
if (!r.ok) {
|
||||||
|
console.error(await r.text());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await r.json();
|
||||||
|
this.renderTiles(data.items);
|
||||||
|
this.updateNav(data.pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Rendering -------------------------------------------------
|
||||||
|
renderTiles(items) {
|
||||||
|
const grid = this.shadowRoot.getElementById("grid");
|
||||||
|
grid.innerHTML = "";
|
||||||
|
items.forEach((item) => {
|
||||||
|
const tile = document.createElement("div");
|
||||||
|
tile.className = "tile";
|
||||||
|
|
||||||
|
if (item.type === "directory") {
|
||||||
|
tile.innerHTML = `<div class="icon">π</div><div>${item.name}</div>`;
|
||||||
|
tile.addEventListener("click", () => {
|
||||||
|
this.path = item.path;
|
||||||
|
this.offset = 0;
|
||||||
|
this.load();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (item.mimetype?.startsWith("image/")) {
|
||||||
|
tile.innerHTML = `<img class="thumb" src="${item.url}" alt="${item.name}"><div>${item.name}</div>`;
|
||||||
|
} else {
|
||||||
|
tile.innerHTML = `<div class="icon">π</div><div>${item.name}</div>`;
|
||||||
|
}
|
||||||
|
tile.addEventListener("click", () => window.open(item.url, "_blank"));
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.appendChild(tile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Navigation + pagination ----------------------------------
|
||||||
|
updateNav({ offset, limit, total }) {
|
||||||
|
this.shadowRoot.getElementById("crumb").textContent = `/${this.path}`;
|
||||||
|
this.shadowRoot.getElementById("prev").disabled = offset === 0;
|
||||||
|
this.shadowRoot.getElementById("next").disabled = offset + limit >= total;
|
||||||
|
this.shadowRoot.getElementById("up").disabled = this.path === "";
|
||||||
|
}
|
||||||
|
|
||||||
|
goUp() {
|
||||||
|
if (!this.path) return;
|
||||||
|
this.path = this.path.split("/").slice(0, -1).join("/");
|
||||||
|
this.offset = 0;
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("file-manager", FileBrowser);
|
||||||
@@ -40,7 +40,7 @@ class GenericField extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set value(val) {
|
set value(val) {
|
||||||
val = val ?? '';
|
val = val ?? "";
|
||||||
this.inputElement.value = val;
|
this.inputElement.value = val;
|
||||||
this.inputElement.setAttribute("value", val);
|
this.inputElement.setAttribute("value", val);
|
||||||
}
|
}
|
||||||
@@ -62,9 +62,9 @@ class GenericField extends HTMLElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({mode: 'open'});
|
this.attachShadow({ mode: "open" });
|
||||||
this.container = document.createElement('div');
|
this.container = document.createElement("div");
|
||||||
this.styleElement = document.createElement('style');
|
this.styleElement = document.createElement("style");
|
||||||
this.styleElement.innerHTML = `
|
this.styleElement.innerHTML = `
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@@ -174,7 +174,7 @@ class GenericField extends HTMLElement {
|
|||||||
|
|
||||||
if (this.inputElement == null && this.field) {
|
if (this.inputElement == null && this.field) {
|
||||||
this.inputElement = document.createElement(this.field.tag);
|
this.inputElement = document.createElement(this.field.tag);
|
||||||
if (this.field.tag === 'button' && this.field.value === "submit") {
|
if (this.field.tag === "button" && this.field.value === "submit") {
|
||||||
this.action = this.field.value;
|
this.action = this.field.value;
|
||||||
}
|
}
|
||||||
this.inputElement.name = this.field.name;
|
this.inputElement.name = this.field.name;
|
||||||
@@ -182,13 +182,19 @@ class GenericField extends HTMLElement {
|
|||||||
|
|
||||||
const me = this;
|
const me = this;
|
||||||
this.inputElement.addEventListener("keyup", (e) => {
|
this.inputElement.addEventListener("keyup", (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === "Enter") {
|
||||||
const event = new CustomEvent("change", {detail: me, bubbles: true});
|
const event = new CustomEvent("change", {
|
||||||
|
detail: me,
|
||||||
|
bubbles: true,
|
||||||
|
});
|
||||||
me.dispatchEvent(event);
|
me.dispatchEvent(event);
|
||||||
|
|
||||||
me.dispatchEvent(new Event("submit"));
|
me.dispatchEvent(new Event("submit"));
|
||||||
} else if (me.field.value !== e.target.value) {
|
} else if (me.field.value !== e.target.value) {
|
||||||
const event = new CustomEvent("change", {detail: me, bubbles: true});
|
const event = new CustomEvent("change", {
|
||||||
|
detail: me,
|
||||||
|
bubbles: true,
|
||||||
|
});
|
||||||
me.dispatchEvent(event);
|
me.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -198,10 +204,17 @@ class GenericField extends HTMLElement {
|
|||||||
me.dispatchEvent(event);
|
me.dispatchEvent(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.inputElement.addEventListener("blur", (e) => {
|
this.inputElement.addEventListener(
|
||||||
const event = new CustomEvent("change", {detail: me, bubbles: true});
|
"blur",
|
||||||
|
(e) => {
|
||||||
|
const event = new CustomEvent("change", {
|
||||||
|
detail: me,
|
||||||
|
bubbles: true,
|
||||||
|
});
|
||||||
me.dispatchEvent(event);
|
me.dispatchEvent(event);
|
||||||
}, true);
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
this.container.appendChild(this.inputElement);
|
this.container.appendChild(this.inputElement);
|
||||||
}
|
}
|
||||||
@@ -210,8 +223,8 @@ class GenericField extends HTMLElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.inputElement.setAttribute("type", this.field.type ?? 'input');
|
this.inputElement.setAttribute("type", this.field.type ?? "input");
|
||||||
this.inputElement.setAttribute("name", this.field.name ?? '');
|
this.inputElement.setAttribute("name", this.field.name ?? "");
|
||||||
|
|
||||||
if (this.field.text != null) {
|
if (this.field.text != null) {
|
||||||
this.inputElement.innerText = this.field.text;
|
this.inputElement.innerText = this.field.text;
|
||||||
@@ -239,14 +252,14 @@ class GenericField extends HTMLElement {
|
|||||||
this.inputElement.removeAttribute("required");
|
this.inputElement.removeAttribute("required");
|
||||||
}
|
}
|
||||||
if (!this.footerElement) {
|
if (!this.footerElement) {
|
||||||
this.footerElement = document.createElement('div');
|
this.footerElement = document.createElement("div");
|
||||||
this.footerElement.style.clear = 'both';
|
this.footerElement.style.clear = "both";
|
||||||
this.container.appendChild(this.footerElement);
|
this.container.appendChild(this.footerElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('generic-field', GenericField);
|
customElements.define("generic-field", GenericField);
|
||||||
|
|
||||||
class GenericForm extends HTMLElement {
|
class GenericForm extends HTMLElement {
|
||||||
fields = {};
|
fields = {};
|
||||||
@@ -254,7 +267,7 @@ class GenericForm extends HTMLElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({mode: 'open'});
|
this.attachShadow({ mode: "open" });
|
||||||
this.styleElement = document.createElement("style");
|
this.styleElement = document.createElement("style");
|
||||||
this.styleElement.innerHTML = `
|
this.styleElement.innerHTML = `
|
||||||
|
|
||||||
@@ -281,27 +294,29 @@ class GenericForm extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
this.container = document.createElement('div');
|
this.container = document.createElement("div");
|
||||||
this.container.appendChild(this.styleElement);
|
this.container.appendChild(this.styleElement);
|
||||||
this.container.classList.add("generic-form-container");
|
this.container.classList.add("generic-form-container");
|
||||||
this.shadowRoot.appendChild(this.container);
|
this.shadowRoot.appendChild(this.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
const preloadedForm = this.getAttribute('preloaded-structure');
|
const preloadedForm = this.getAttribute("preloaded-structure");
|
||||||
if (preloadedForm) {
|
if (preloadedForm) {
|
||||||
try {
|
try {
|
||||||
const form = JSON.parse(preloadedForm);
|
const form = JSON.parse(preloadedForm);
|
||||||
this.constructForm(form)
|
this.constructForm(form);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error, preloadedForm);
|
console.error(error, preloadedForm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const url = this.getAttribute('url');
|
const url = this.getAttribute("url");
|
||||||
if (url) {
|
if (url) {
|
||||||
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get");
|
const fullUrl = url.startsWith("/")
|
||||||
|
? window.location.origin + url
|
||||||
|
: new URL(window.location.origin + "/http-get");
|
||||||
if (!url.startsWith("/")) {
|
if (!url.startsWith("/")) {
|
||||||
fullUrl.searchParams.set('url', url);
|
fullUrl.searchParams.set("url", url);
|
||||||
}
|
}
|
||||||
this.loadForm(fullUrl.toString());
|
this.loadForm(fullUrl.toString());
|
||||||
} else {
|
} else {
|
||||||
@@ -318,10 +333,10 @@ class GenericForm extends HTMLElement {
|
|||||||
let hasAutoFocus = Object.keys(this.fields).length !== 0;
|
let hasAutoFocus = Object.keys(this.fields).length !== 0;
|
||||||
|
|
||||||
fields.sort((a, b) => a.index - b.index);
|
fields.sort((a, b) => a.index - b.index);
|
||||||
fields.forEach(field => {
|
fields.forEach((field) => {
|
||||||
const updatingField = field.name in this.fields
|
const updatingField = field.name in this.fields;
|
||||||
|
|
||||||
this.fields[field.name] ??= document.createElement('generic-field');
|
this.fields[field.name] ??= document.createElement("generic-field");
|
||||||
|
|
||||||
const fieldElement = this.fields[field.name];
|
const fieldElement = this.fields[field.name];
|
||||||
|
|
||||||
@@ -362,7 +377,7 @@ class GenericForm extends HTMLElement {
|
|||||||
window.location.pathname = saveResult.redirect_url;
|
window.location.pathname = saveResult.redirect_url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -374,7 +389,9 @@ class GenericForm extends HTMLElement {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
|
throw new Error(
|
||||||
|
`Failed to fetch: ${response.status} ${response.statusText}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.constructForm(await response.json());
|
await this.constructForm(await response.json());
|
||||||
@@ -387,15 +404,15 @@ class GenericForm extends HTMLElement {
|
|||||||
const url = this.getAttribute("url");
|
const url = this.getAttribute("url");
|
||||||
|
|
||||||
let response = await fetch(url, {
|
let response = await fetch(url, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({"action": "validate", "form": this.form})
|
body: JSON.stringify({ action: "validate", form: this.form }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = await response.json();
|
const form = await response.json();
|
||||||
Object.values(form.fields).forEach(field => {
|
Object.values(form.fields).forEach((field) => {
|
||||||
if (!this.form.fields[field.name]) {
|
if (!this.form.fields[field.name]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -409,23 +426,23 @@ class GenericForm extends HTMLElement {
|
|||||||
this.fields[field.name].setAttribute("field", field);
|
this.fields[field.name].setAttribute("field", field);
|
||||||
this.fields[field.name].updateAttributes();
|
this.fields[field.name].updateAttributes();
|
||||||
});
|
});
|
||||||
Object.values(form.fields).forEach(field => {
|
Object.values(form.fields).forEach((field) => {
|
||||||
this.fields[field.name].setErrors(field.errors);
|
this.fields[field.name].setErrors(field.errors);
|
||||||
});
|
});
|
||||||
return form['is_valid'];
|
return form["is_valid"];
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
const url = this.getAttribute("url");
|
const url = this.getAttribute("url");
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({"action": "submit", "form": this.form})
|
body: JSON.stringify({ action: "submit", form: this.form }),
|
||||||
});
|
});
|
||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('generic-form', GenericForm);
|
customElements.define("generic-form", GenericForm);
|
||||||
|
|||||||
@@ -9,21 +9,23 @@
|
|||||||
class HTMLFrame extends HTMLElement {
|
class HTMLFrame extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: "open" });
|
||||||
this.container = document.createElement('div');
|
this.container = document.createElement("div");
|
||||||
this.shadowRoot.appendChild(this.container);
|
this.shadowRoot.appendChild(this.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.container.classList.add("html_frame");
|
this.container.classList.add("html_frame");
|
||||||
let url = this.getAttribute('url');
|
let url = this.getAttribute("url");
|
||||||
if (!url.startsWith("https")) {
|
if (!url.startsWith("https")) {
|
||||||
url = "https://" + url;
|
url = "https://" + url;
|
||||||
}
|
}
|
||||||
if (url) {
|
if (url) {
|
||||||
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get");
|
const fullUrl = url.startsWith("/")
|
||||||
|
? window.location.origin + url
|
||||||
|
: new URL(window.location.origin + "/http-get");
|
||||||
if (!url.startsWith("/")) {
|
if (!url.startsWith("/")) {
|
||||||
fullUrl.searchParams.set('url', url);
|
fullUrl.searchParams.set("url", url);
|
||||||
}
|
}
|
||||||
this.loadAndRender(fullUrl.toString());
|
this.loadAndRender(fullUrl.toString());
|
||||||
} else {
|
} else {
|
||||||
@@ -39,7 +41,7 @@ class HTMLFrame extends HTMLElement {
|
|||||||
}
|
}
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
if (url.endsWith(".md")) {
|
if (url.endsWith(".md")) {
|
||||||
const markdownElement = document.createElement('div');
|
const markdownElement = document.createElement("div");
|
||||||
markdownElement.innerHTML = html;
|
markdownElement.innerHTML = html;
|
||||||
this.outerHTML = html;
|
this.outerHTML = html;
|
||||||
} else {
|
} else {
|
||||||
@@ -51,4 +53,4 @@ class HTMLFrame extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('html-frame', HTMLFrame);
|
customElements.define("html-frame", HTMLFrame);
|
||||||
|
|||||||
@@ -12,22 +12,22 @@
|
|||||||
class HTMLFrame extends HTMLElement {
|
class HTMLFrame extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: "open" });
|
||||||
this.container = document.createElement('div');
|
this.container = document.createElement("div");
|
||||||
this.shadowRoot.appendChild(this.container);
|
this.shadowRoot.appendChild(this.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.container.classList.add('html_frame');
|
this.container.classList.add("html_frame");
|
||||||
const url = this.getAttribute('url');
|
const url = this.getAttribute("url");
|
||||||
if (url) {
|
if (url) {
|
||||||
const fullUrl = url.startsWith('/')
|
const fullUrl = url.startsWith("/")
|
||||||
? window.location.origin + url
|
? window.location.origin + url
|
||||||
: new URL(window.location.origin + '/http-get');
|
: new URL(window.location.origin + "/http-get");
|
||||||
if (!url.startsWith('/')) fullUrl.searchParams.set('url', url);
|
if (!url.startsWith("/")) fullUrl.searchParams.set("url", url);
|
||||||
this.loadAndRender(fullUrl.toString());
|
this.loadAndRender(fullUrl.toString());
|
||||||
} else {
|
} else {
|
||||||
this.container.textContent = 'No source URL!';
|
this.container.textContent = "No source URL!";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,4 +45,4 @@ class HTMLFrame extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('markdown-frame', HTMLFrame);
|
customElements.define("markdown-frame", HTMLFrame);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
// No external libraries or dependencies are used other than standard web components.
|
// No external libraries or dependencies are used other than standard web components.
|
||||||
|
|
||||||
|
|
||||||
// MIT License
|
// MIT License
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
@@ -13,19 +12,18 @@
|
|||||||
//
|
//
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
class TileGridElement extends HTMLElement {
|
class TileGridElement extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: "open" });
|
||||||
this.gridId = this.getAttribute('grid');
|
this.gridId = this.getAttribute("grid");
|
||||||
this.component = document.createElement('div');
|
this.component = document.createElement("div");
|
||||||
this.shadowRoot.appendChild(this.component);
|
this.shadowRoot.appendChild(this.component);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
console.log('connected');
|
console.log("connected");
|
||||||
this.styleElement = document.createElement('style');
|
this.styleElement = document.createElement("style");
|
||||||
this.styleElement.textContent = `
|
this.styleElement.textContent = `
|
||||||
.grid {
|
.grid {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -48,26 +46,26 @@ class TileGridElement extends HTMLElement {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
this.component.appendChild(this.styleElement);
|
this.component.appendChild(this.styleElement);
|
||||||
this.container = document.createElement('div');
|
this.container = document.createElement("div");
|
||||||
this.container.classList.add('gallery');
|
this.container.classList.add("gallery");
|
||||||
this.component.appendChild(this.container);
|
this.component.appendChild(this.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
addImage(src) {
|
addImage(src) {
|
||||||
const item = document.createElement('img');
|
const item = document.createElement("img");
|
||||||
item.src = src;
|
item.src = src;
|
||||||
item.classList.add('tile');
|
item.classList.add("tile");
|
||||||
item.style.width = '100px';
|
item.style.width = "100px";
|
||||||
item.style.height = '100px';
|
item.style.height = "100px";
|
||||||
this.container.appendChild(item);
|
this.container.appendChild(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
addImages(srcs) {
|
addImages(srcs) {
|
||||||
srcs.forEach(src => this.addImage(src));
|
srcs.forEach((src) => this.addImage(src));
|
||||||
}
|
}
|
||||||
|
|
||||||
addElement(element) {
|
addElement(element) {
|
||||||
element.classList.add('tile');
|
element.classList.add("tile");
|
||||||
this.container.appendChild(element);
|
this.container.appendChild(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,14 +73,14 @@ class TileGridElement extends HTMLElement {
|
|||||||
class UploadButton extends HTMLElement {
|
class UploadButton extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: "open" });
|
||||||
this.component = document.createElement('div');
|
this.component = document.createElement("div");
|
||||||
this.shadowRoot.appendChild(this.component);
|
this.shadowRoot.appendChild(this.component);
|
||||||
window.u = this;
|
window.u = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
get gridSelector() {
|
get gridSelector() {
|
||||||
return this.getAttribute('grid');
|
return this.getAttribute("grid");
|
||||||
}
|
}
|
||||||
grid = null;
|
grid = null;
|
||||||
|
|
||||||
@@ -91,8 +89,8 @@ class UploadButton extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
console.log('connected');
|
console.log("connected");
|
||||||
this.styleElement = document.createElement('style');
|
this.styleElement = document.createElement("style");
|
||||||
this.styleElement.textContent = `
|
this.styleElement.textContent = `
|
||||||
.upload-button {
|
.upload-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -116,14 +114,14 @@ class UploadButton extends HTMLElement {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
this.shadowRoot.appendChild(this.styleElement);
|
this.shadowRoot.appendChild(this.styleElement);
|
||||||
this.container = document.createElement('div');
|
this.container = document.createElement("div");
|
||||||
this.container.classList.add('upload-button');
|
this.container.classList.add("upload-button");
|
||||||
this.shadowRoot.appendChild(this.container);
|
this.shadowRoot.appendChild(this.container);
|
||||||
const input = document.createElement('input');
|
const input = document.createElement("input");
|
||||||
input.type = 'file';
|
input.type = "file";
|
||||||
input.accept = 'image/*';
|
input.accept = "image/*";
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
input.addEventListener('change', (e) => {
|
input.addEventListener("change", (e) => {
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
const urls = [];
|
const urls = [];
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
@@ -137,39 +135,39 @@ class UploadButton extends HTMLElement {
|
|||||||
reader.readAsDataURL(files[i]);
|
reader.readAsDataURL(files[i]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const label = document.createElement('label');
|
const label = document.createElement("label");
|
||||||
label.textContent = 'Upload Images';
|
label.textContent = "Upload Images";
|
||||||
label.appendChild(input);
|
label.appendChild(input);
|
||||||
this.container.appendChild(label);
|
this.container.appendChild(label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('upload-button', UploadButton);
|
customElements.define("upload-button", UploadButton);
|
||||||
customElements.define('tile-grid', TileGridElement);
|
customElements.define("tile-grid", TileGridElement);
|
||||||
|
|
||||||
class MeniaUploadElement extends HTMLElement {
|
class MeniaUploadElement extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: "open" });
|
||||||
this.component = document.createElement("div");
|
this.component = document.createElement("div");
|
||||||
alert('aaaa');
|
alert("aaaa");
|
||||||
this.shadowRoot.appendChild(this.component);
|
this.shadowRoot.appendChild(this.component);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.container = document.createElement("div");
|
this.container = document.createElement("div");
|
||||||
this.component.style.height = '100%';
|
this.component.style.height = "100%";
|
||||||
this.component.style.backgroundColor = 'blue';
|
this.component.style.backgroundColor = "blue";
|
||||||
this.shadowRoot.appendChild(this.container);
|
this.shadowRoot.appendChild(this.container);
|
||||||
|
|
||||||
this.tileElement = document.createElement("tile-grid");
|
this.tileElement = document.createElement("tile-grid");
|
||||||
this.tileElement.style.backgroundColor = 'red';
|
this.tileElement.style.backgroundColor = "red";
|
||||||
this.tileElement.style.height = '100%';
|
this.tileElement.style.height = "100%";
|
||||||
this.component.appendChild(this.tileElement);
|
this.component.appendChild(this.tileElement);
|
||||||
|
|
||||||
this.uploadButton = document.createElement('upload-button');
|
this.uploadButton = document.createElement("upload-button");
|
||||||
this.component.appendChild(this.uploadButton);
|
this.component.appendChild(this.uploadButton);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('menia-upload', MeniaUploadElement);
|
customElements.define("menia-upload", MeniaUploadElement);
|
||||||
|
|||||||
@@ -22,18 +22,17 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
class MessageListManagerElement extends HTMLElement {
|
class MessageListManagerElement extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: "open" });
|
||||||
this.container = document.createElement("div");
|
this.container = document.createElement("div");
|
||||||
this.shadowRoot.appendChild(this.container);
|
this.shadowRoot.appendChild(this.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
const channels = await app.rpc.getChannels();
|
const channels = await app.rpc.getChannels();
|
||||||
channels.forEach(channel => {
|
channels.forEach((channel) => {
|
||||||
const messageList = document.createElement("message-list");
|
const messageList = document.createElement("message-list");
|
||||||
messageList.setAttribute("channel", channel.uid);
|
messageList.setAttribute("channel", channel.uid);
|
||||||
this.container.appendChild(messageList);
|
this.container.appendChild(messageList);
|
||||||
|
|||||||
+84
-147
@@ -5,166 +5,103 @@
|
|||||||
// The code seems to rely on some external dependencies like 'models.Message', 'app', and 'Schedule'. These should be imported or defined elsewhere in your application.
|
// The code seems to rely on some external dependencies like 'models.Message', 'app', and 'Schedule'. These should be imported or defined elsewhere in your application.
|
||||||
|
|
||||||
// MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty.
|
// MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty.
|
||||||
|
import { app } from "../app.js";
|
||||||
class MessageListElement extends HTMLElement {
|
class MessageList extends HTMLElement {
|
||||||
static get observedAttributes() {
|
|
||||||
return ["messages"];
|
|
||||||
}
|
|
||||||
|
|
||||||
messages = [];
|
|
||||||
room = null;
|
|
||||||
url = null;
|
|
||||||
container = null;
|
|
||||||
messageEventSchedule = null;
|
|
||||||
observer = null;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
app.ws.addEventListener("update_message_text", (data) => {
|
||||||
this.component = document.createElement('div');
|
this.updateMessageText(data.uid, data);
|
||||||
this.shadowRoot.appendChild(this.component);
|
});
|
||||||
}
|
app.ws.addEventListener("set_typing", (data) => {
|
||||||
|
this.triggerGlow(data.user_uid,data.color);
|
||||||
linkifyText(text) {
|
|
||||||
const urlRegex = /https?:\/\/[^\s]+/g;
|
|
||||||
return text.replace(urlRegex, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
timeAgo(date1, date2) {
|
|
||||||
const diffMs = Math.abs(date2 - date1);
|
|
||||||
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
||||||
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
|
||||||
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
||||||
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
|
||||||
|
|
||||||
if (days) {
|
|
||||||
return `${days} ${days > 1 ? 'days' : 'day'} ago`;
|
|
||||||
}
|
|
||||||
if (hours) {
|
|
||||||
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`;
|
|
||||||
}
|
|
||||||
if (minutes) {
|
|
||||||
return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`;
|
|
||||||
}
|
|
||||||
return 'just now';
|
|
||||||
}
|
|
||||||
|
|
||||||
timeDescription(isoDate) {
|
|
||||||
const date = new Date(isoDate);
|
|
||||||
const hours = String(date.getHours()).padStart(2, "0");
|
|
||||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
||||||
let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
|
|
||||||
return timeStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
createElement(message) {
|
|
||||||
const element = document.createElement("div");
|
|
||||||
element.dataset.uid = message.uid;
|
|
||||||
element.dataset.color = message.color;
|
|
||||||
element.dataset.channel_uid = message.channel_uid;
|
|
||||||
element.dataset.user_nick = message.user_nick;
|
|
||||||
element.dataset.created_at = message.created_at;
|
|
||||||
element.dataset.user_uid = message.user_uid;
|
|
||||||
element.dataset.message = message.message;
|
|
||||||
|
|
||||||
element.classList.add("message");
|
|
||||||
if (!this.messages.length || this.messages[this.messages.length - 1].user_uid != message.user_uid) {
|
|
||||||
element.classList.add("switch-user");
|
|
||||||
}
|
|
||||||
|
|
||||||
const avatar = document.createElement("div");
|
|
||||||
avatar.classList.add("avatar");
|
|
||||||
avatar.classList.add("no-select");
|
|
||||||
avatar.style.backgroundColor = message.color;
|
|
||||||
avatar.style.color = "black";
|
|
||||||
avatar.innerText = message.user_nick[0];
|
|
||||||
|
|
||||||
const messageContent = document.createElement("div");
|
|
||||||
messageContent.classList.add("message-content");
|
|
||||||
|
|
||||||
const author = document.createElement("div");
|
|
||||||
author.classList.add("author");
|
|
||||||
author.style.color = message.color;
|
|
||||||
author.textContent = message.user_nick;
|
|
||||||
|
|
||||||
const text = document.createElement("div");
|
|
||||||
text.classList.add("text");
|
|
||||||
if (message.html) text.innerHTML = message.html;
|
|
||||||
|
|
||||||
const time = document.createElement("div");
|
|
||||||
time.classList.add("time");
|
|
||||||
time.dataset.created_at = message.created_at;
|
|
||||||
time.textContent = this.timeDescription(message.created_at);
|
|
||||||
|
|
||||||
messageContent.appendChild(author);
|
|
||||||
messageContent.appendChild(text);
|
|
||||||
messageContent.appendChild(time);
|
|
||||||
|
|
||||||
element.appendChild(avatar);
|
|
||||||
element.appendChild(messageContent);
|
|
||||||
|
|
||||||
message.element = element;
|
|
||||||
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
addMessage(message) {
|
|
||||||
const obj = new models.Message(
|
|
||||||
message.uid,
|
|
||||||
message.channel_uid,
|
|
||||||
message.user_uid,
|
|
||||||
message.user_nick,
|
|
||||||
message.color,
|
|
||||||
message.message,
|
|
||||||
message.html,
|
|
||||||
message.created_at,
|
|
||||||
message.updated_at
|
|
||||||
);
|
|
||||||
|
|
||||||
const element = this.createElement(obj);
|
|
||||||
this.messages.push(obj);
|
|
||||||
this.container.appendChild(element);
|
|
||||||
|
|
||||||
this.messageEventSchedule.delay(() => {
|
|
||||||
this.dispatchEvent(new CustomEvent("message", { detail: obj, bubbles: true }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return obj;
|
this.items = [];
|
||||||
}
|
|
||||||
|
|
||||||
scrollBottom() {
|
|
||||||
this.container.scrollTop = this.container.scrollHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
const link = document.createElement('link');
|
const messagesContainer = this
|
||||||
link.rel = 'stylesheet';
|
messagesContainer.addEventListener('click', (e) => {
|
||||||
link.href = '/base.css';
|
if (e.target.tagName !== 'IMG' || e.target.classList.contains('avatar-img')) return;
|
||||||
this.component.appendChild(link);
|
const img = e.target;
|
||||||
this.component.classList.add("chat-messages");
|
const overlay = document.createElement('div');
|
||||||
|
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;'
|
||||||
|
const fullImg = document.createElement('img');
|
||||||
|
const urlObj = new URL(img.src); urlObj.search = '';
|
||||||
|
fullImg.src = urlObj.toString();
|
||||||
|
fullImg.alt = img.alt;
|
||||||
|
fullImg.style.maxWidth = '90%';
|
||||||
|
fullImg.style.maxHeight = '90%';
|
||||||
|
overlay.appendChild(fullImg);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
overlay.addEventListener('click', () => document.body.removeChild(overlay));
|
||||||
|
})
|
||||||
|
|
||||||
this.container = document.createElement('div');
|
}
|
||||||
this.component.appendChild(this.container);
|
isElementVisible(element) {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.left >= 0 &&
|
||||||
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||||
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
isScrolledToBottom() {
|
||||||
|
return this.isElementVisible(this.querySelector(".message-list-bottom"));
|
||||||
|
}
|
||||||
|
scrollToBottom(force) {
|
||||||
|
this.scrollTop = this.scrollHeight;
|
||||||
|
|
||||||
this.messageEventSchedule = new Schedule(500);
|
this.querySelector(".message-list-bottom").scrollIntoView();
|
||||||
this.messages = [];
|
setTimeout(() => {
|
||||||
this.channel_uid = this.getAttribute("channel");
|
|
||||||
|
|
||||||
app.addEventListener(this.channel_uid, (data) => {
|
this.scrollTop = this.scrollHeight;
|
||||||
this.addMessage(data);
|
this.querySelector(".message-list-bottom").scrollIntoView();
|
||||||
});
|
},200)
|
||||||
|
}
|
||||||
|
updateMessageText(uid, message) {
|
||||||
|
const messageDiv = this.querySelector('div[data-uid="' + uid + '"]');
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent("rendered", { detail: this, bubbles: true }));
|
if (!messageDiv) {
|
||||||
|
return;
|
||||||
this.timeUpdateInterval = setInterval(() => {
|
}
|
||||||
this.messages.forEach((message) => {
|
const scrollToBottom = this.isScrolledToBottom();
|
||||||
const newText = this.timeDescription(message.created_at);
|
const receivedHtml = document.createElement("div");
|
||||||
if (newText != message.element.innerText) {
|
receivedHtml.innerHTML = message.html;
|
||||||
message.element.querySelector(".time").innerText = newText;
|
const html = receivedHtml.querySelector(".text").innerHTML;
|
||||||
|
const textElement = messageDiv.querySelector(".text");
|
||||||
|
textElement.innerHTML = html;
|
||||||
|
textElement.style.display = message.text == "" ? "none" : "block";
|
||||||
|
if(scrollToBottom)
|
||||||
|
this.scrollToBottom(true)
|
||||||
|
}
|
||||||
|
triggerGlow(uid,color) {
|
||||||
|
app.starField.glowColor(color)
|
||||||
|
let lastElement = null;
|
||||||
|
this.querySelectorAll(".avatar").forEach((el) => {
|
||||||
|
const div = el.closest("a");
|
||||||
|
if (el.href.indexOf(uid) != -1) {
|
||||||
|
lastElement = el;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 30000);
|
if (lastElement) {
|
||||||
|
lastElement.classList.add("glow");
|
||||||
|
setTimeout(() => {
|
||||||
|
lastElement.classList.remove("glow");
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('message-list', MessageListElement);
|
set data(items) {
|
||||||
|
this.items = items;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
this.innerHTML = "";
|
||||||
|
|
||||||
|
//this.insertAdjacentHTML("beforeend", html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("message-list", MessageList);
|
||||||
|
|||||||
+23
-13
@@ -7,20 +7,30 @@
|
|||||||
// MIT License
|
// MIT License
|
||||||
|
|
||||||
class MessageModel {
|
class MessageModel {
|
||||||
constructor(uid, channel_uid, user_uid, user_nick, color, message, html, created_at, updated_at) {
|
constructor(
|
||||||
this.uid = uid
|
uid,
|
||||||
this.message = message
|
channel_uid,
|
||||||
this.html = html
|
user_uid,
|
||||||
this.user_uid = user_uid
|
user_nick,
|
||||||
this.user_nick = user_nick
|
color,
|
||||||
this.color = color
|
message,
|
||||||
this.channel_uid = channel_uid
|
html,
|
||||||
this.created_at = created_at
|
created_at,
|
||||||
this.updated_at = updated_at
|
updated_at,
|
||||||
this.element = null
|
) {
|
||||||
|
this.uid = uid;
|
||||||
|
this.message = message;
|
||||||
|
this.html = html;
|
||||||
|
this.user_uid = user_uid;
|
||||||
|
this.user_nick = user_nick;
|
||||||
|
this.color = color;
|
||||||
|
this.channel_uid = channel_uid;
|
||||||
|
this.created_at = created_at;
|
||||||
|
this.updated_at = updated_at;
|
||||||
|
this.element = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const models = {
|
const models = {
|
||||||
Message: MessageModel
|
Message: MessageModel,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -12,13 +12,17 @@ this.onpush = (event) => {
|
|||||||
const subscriptionObject = {
|
const subscriptionObject = {
|
||||||
endpoint: pushSubscription.endpoint,
|
endpoint: pushSubscription.endpoint,
|
||||||
keys: {
|
keys: {
|
||||||
p256dh: pushSubscription.getKey('p256dh'),
|
p256dh: pushSubscription.getKey("p256dh"),
|
||||||
auth: pushSubscription.getKey('auth'),
|
auth: pushSubscription.getKey("auth"),
|
||||||
},
|
},
|
||||||
encoding: PushManager.supportedContentEncodings,
|
encoding: PushManager.supportedContentEncodings,
|
||||||
/* other app-specific data, such as user identity */
|
/* other app-specific data, such as user identity */
|
||||||
};
|
};
|
||||||
console.log(pushSubscription.endpoint, pushSubscription, subscriptionObject);
|
console.log(
|
||||||
|
pushSubscription.endpoint,
|
||||||
|
pushSubscription,
|
||||||
|
subscriptionObject,
|
||||||
|
);
|
||||||
// The push subscription details needed by the application
|
// The push subscription details needed by the application
|
||||||
// server are now available, and can be sent to it using,
|
// server are now available, and can be sent to it using,
|
||||||
// for example, the fetch() API.
|
// for example, the fetch() API.
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
:root {
|
||||||
|
--star-color: white;
|
||||||
|
--background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.day {
|
||||||
|
--star-color: #444;
|
||||||
|
--background-color: #e6f0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.night {
|
||||||
|
--star-color: white;
|
||||||
|
--background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
transition: background-color 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--star-color);
|
||||||
|
animation: twinkle 2s infinite ease-in-out;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes twinkle {
|
||||||
|
0%, 100% { opacity: 0.8; transform: scale(1); }
|
||||||
|
50% { opacity: 1; transform: scale(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#themeToggle {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star.special {
|
||||||
|
box-shadow: 0 0 10px 3px gold;
|
||||||
|
transform: scale(1.4);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 12px;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-shadow: 1px 1px 2px black;
|
||||||
|
display: none;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
.star-popup {
|
||||||
|
position: absolute;
|
||||||
|
max-width: 300px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 10000;
|
||||||
|
text-shadow: 1px 1px 3px black;
|
||||||
|
display: none;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.star:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-popup {
|
||||||
|
position: absolute;
|
||||||
|
max-width: 300px;
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
|
||||||
|
z-index: 10000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-popup h3 {
|
||||||
|
margin: 0 0 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-popup button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 3em;
|
||||||
|
color: white;
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 0 20px rgba(0,0,0,0.8);
|
||||||
|
z-index: 9999;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.6s ease;
|
||||||
|
max-width: 80vw;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes demoFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -60%) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes demoPulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 rgba(255, 255, 150, 0);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
box-shadow: 0 0 30px 15px rgba(255, 255, 150, 0.9);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 rgba(255, 255, 150, 0);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-highlight {
|
||||||
|
animation: demoPulse 1.5s ease-out;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-notify-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 50px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-notify {
|
||||||
|
opacity: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 300;
|
||||||
|
text-shadow: 0 0 10px rgba(0,0,0,0.7);
|
||||||
|
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,33 +1,34 @@
|
|||||||
async function requestNotificationPermission() {
|
async function requestNotificationPermission() {
|
||||||
const permission = await Notification.requestPermission();
|
const permission = await Notification.requestPermission();
|
||||||
return permission === 'granted';
|
return permission === "granted";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to Push Notifications
|
// Subscribe to Push Notifications
|
||||||
async function subscribeUser() {
|
async function subscribeUser() {
|
||||||
const registration = await navigator.serviceWorker.register('/service-worker.js');
|
const registration =
|
||||||
|
await navigator.serviceWorker.register("/service-worker.js");
|
||||||
|
|
||||||
const subscription = await registration.pushManager.subscribe({
|
const subscription = await registration.pushManager.subscribe({
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
|
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send subscription to your backend
|
// Send subscription to your backend
|
||||||
await fetch('/subscribe', {
|
await fetch("/subscribe", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: JSON.stringify(subscription),
|
body: JSON.stringify(subscription),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service Worker (service-worker.js)
|
// Service Worker (service-worker.js)
|
||||||
self.addEventListener('push', event => {
|
self.addEventListener("push", (event) => {
|
||||||
const data = event.data.json();
|
const data = event.data.json();
|
||||||
self.registration.showNotification(data.title, {
|
self.registration.showNotification(data.title, {
|
||||||
body: data.message,
|
body: data.message,
|
||||||
icon: data.icon
|
icon: data.icon,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+36
-22
@@ -4,19 +4,21 @@ export class Socket extends EventHandler {
|
|||||||
/**
|
/**
|
||||||
* @type {URL}
|
* @type {URL}
|
||||||
*/
|
*/
|
||||||
url
|
url;
|
||||||
/**
|
/**
|
||||||
* @type {WebSocket|null}
|
* @type {WebSocket|null}
|
||||||
*/
|
*/
|
||||||
ws = null
|
ws = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {null|PromiseWithResolvers<Socket>&{resolved?:boolean}}
|
* @type {null|PromiseWithResolvers<Socket>&{resolved?:boolean}}
|
||||||
*/
|
*/
|
||||||
connection = null
|
connection = null;
|
||||||
|
|
||||||
shouldReconnect = true;
|
shouldReconnect = true;
|
||||||
|
|
||||||
|
_debug = false;
|
||||||
|
|
||||||
get isConnected() {
|
get isConnected() {
|
||||||
return this.ws && this.ws.readyState === WebSocket.OPEN;
|
return this.ws && this.ws.readyState === WebSocket.OPEN;
|
||||||
}
|
}
|
||||||
@@ -28,10 +30,10 @@ export class Socket extends EventHandler {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.url = new URL('/rpc.ws', window.location.origin);
|
this.url = new URL("/rpc.ws", window.location.origin);
|
||||||
this.url.protocol = this.url.protocol.replace('http', 'ws');
|
this.url.protocol = this.url.protocol.replace("http", "ws");
|
||||||
|
|
||||||
this.connect()
|
this.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
@@ -40,7 +42,7 @@ export class Socket extends EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.connection || this.connection.resolved) {
|
if (!this.connection || this.connection.resolved) {
|
||||||
this.connection = Promise.withResolvers()
|
this.connection = Promise.withResolvers();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws = new WebSocket(this.url);
|
this.ws = new WebSocket(this.url);
|
||||||
@@ -52,12 +54,12 @@ export class Socket extends EventHandler {
|
|||||||
|
|
||||||
this.ws.addEventListener("close", () => {
|
this.ws.addEventListener("close", () => {
|
||||||
console.log("Connection closed");
|
console.log("Connection closed");
|
||||||
this.disconnect()
|
this.disconnect();
|
||||||
})
|
});
|
||||||
this.ws.addEventListener("error", (e) => {
|
this.ws.addEventListener("error", (e) => {
|
||||||
console.error("Connection error", e);
|
console.error("Connection error", e);
|
||||||
this.disconnect()
|
this.disconnect();
|
||||||
})
|
});
|
||||||
this.ws.addEventListener("message", (e) => {
|
this.ws.addEventListener("message", (e) => {
|
||||||
if (e.data instanceof Blob || e.data instanceof ArrayBuffer) {
|
if (e.data instanceof Blob || e.data instanceof ArrayBuffer) {
|
||||||
console.error("Binary data not supported");
|
console.error("Binary data not supported");
|
||||||
@@ -68,10 +70,9 @@ export class Socket extends EventHandler {
|
|||||||
console.error("Failed to parse message", e);
|
console.error("Failed to parse message", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
onData(data) {
|
onData(data) {
|
||||||
if (data.success !== undefined && !data.success) {
|
if (data.success !== undefined && !data.success) {
|
||||||
console.error(data);
|
console.error(data);
|
||||||
@@ -81,7 +82,11 @@ export class Socket extends EventHandler {
|
|||||||
}
|
}
|
||||||
if (data.channel_uid) {
|
if (data.channel_uid) {
|
||||||
this.emit(data.channel_uid, data.data);
|
this.emit(data.channel_uid, data.data);
|
||||||
this.emit("channel-message", data);
|
if (!data["event"]) this.emit("channel-message", data);
|
||||||
|
}
|
||||||
|
this.emit("data", data.data);
|
||||||
|
if (data["event"]) {
|
||||||
|
this.emit(data.event, data.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,27 +94,36 @@ export class Socket extends EventHandler {
|
|||||||
this.ws?.close();
|
this.ws?.close();
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
|
|
||||||
if (this.shouldReconnect) setTimeout(() => {
|
if (this.shouldReconnect)
|
||||||
|
setTimeout(() => {
|
||||||
console.log("Reconnecting");
|
console.log("Reconnecting");
|
||||||
|
this.emit("reconnecting");
|
||||||
return this.connect();
|
return this.connect();
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_camelToSnake(str) {
|
_camelToSnake(str) {
|
||||||
return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
get client() {
|
get client() {
|
||||||
const me = this;
|
const me = this;
|
||||||
return new Proxy({}, {
|
return new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
get(_, prop) {
|
get(_, prop) {
|
||||||
return (...args) => {
|
return (...args) => {
|
||||||
const functionName = me._camelToSnake(prop);
|
const functionName = me._camelToSnake(prop);
|
||||||
|
if(me._debug){
|
||||||
|
const call = {}
|
||||||
|
call[functionName] = args
|
||||||
|
console.debug(call)
|
||||||
|
}
|
||||||
return me.call(functionName, ...args);
|
return me.call(functionName, ...args);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateCallId() {
|
generateCallId() {
|
||||||
@@ -117,7 +131,7 @@ export class Socket extends EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendJson(data) {
|
async sendJson(data) {
|
||||||
await this.connect().then(api => {
|
await this.connect().then((api) => {
|
||||||
api.ws.send(JSON.stringify(data));
|
api.ws.send(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -128,9 +142,9 @@ export class Socket extends EventHandler {
|
|||||||
method,
|
method,
|
||||||
args,
|
args,
|
||||||
};
|
};
|
||||||
const me = this
|
const me = this;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
me.addEventListener(call.callId, data => resolve(data));
|
me.addEventListener(call.callId, (data) => resolve(data));
|
||||||
me.sendJson(call);
|
me.sendJson(call);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,17 @@
|
|||||||
|
|
||||||
// This class defines a custom HTML element for an upload button with integrated file upload functionality using XMLHttpRequest.
|
// This class defines a custom HTML element for an upload button with integrated file upload functionality using XMLHttpRequest.
|
||||||
|
|
||||||
|
|
||||||
// MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
// MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
class UploadButtonElement extends HTMLElement {
|
class UploadButtonElement extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: "open" });
|
||||||
}
|
}
|
||||||
chatInput = null
|
chatInput = null;
|
||||||
async uploadFiles() {
|
async uploadFiles() {
|
||||||
const fileInput = this.container.querySelector('.file-input');
|
const fileInput = this.container.querySelector(".file-input");
|
||||||
const uploadButton = this.container.querySelector('.upload-button');
|
const uploadButton = this.container.querySelector(".upload-button");
|
||||||
|
|
||||||
if (!fileInput.files.length) {
|
if (!fileInput.files.length) {
|
||||||
return;
|
return;
|
||||||
@@ -21,13 +20,13 @@ class UploadButtonElement extends HTMLElement {
|
|||||||
|
|
||||||
const files = fileInput.files;
|
const files = fileInput.files;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('channel_uid', this.channelUid);
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
formData.append('files[]', files[i]);
|
formData.append("files[]", files[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = new XMLHttpRequest();
|
const request = new XMLHttpRequest();
|
||||||
request.open('POST', '/drive.bin', true);
|
|
||||||
|
request.responseType = "json";
|
||||||
|
request.open("POST", `/channel/${this.channelUid}/attachment.bin`, true);
|
||||||
|
|
||||||
request.upload.onprogress = function (event) {
|
request.upload.onprogress = function (event) {
|
||||||
if (event.lengthComputable) {
|
if (event.lengthComputable) {
|
||||||
@@ -35,26 +34,29 @@ class UploadButtonElement extends HTMLElement {
|
|||||||
uploadButton.innerText = `${Math.round(percentComplete)}%`;
|
uploadButton.innerText = `${Math.round(percentComplete)}%`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const me = this;
|
||||||
request.onload = function () {
|
request.onload = function () {
|
||||||
if (request.status === 200) {
|
if (request.status === 200) {
|
||||||
uploadButton.innerHTML = 'π€';
|
me.dispatchEvent(
|
||||||
|
new CustomEvent("uploaded", { detail: request.response }),
|
||||||
|
);
|
||||||
|
uploadButton.innerHTML = "π€";
|
||||||
} else {
|
} else {
|
||||||
alert('Upload failed');
|
alert("Upload failed");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = function () {
|
request.onerror = function () {
|
||||||
alert('Error while uploading.');
|
alert("Error while uploading.");
|
||||||
};
|
};
|
||||||
|
|
||||||
request.send(formData);
|
request.send(formData);
|
||||||
const uploadEvent = new Event('upload',{});
|
const uploadEvent = new Event("upload", {});
|
||||||
this.dispatchEvent(uploadEvent);
|
this.dispatchEvent(uploadEvent);
|
||||||
}
|
}
|
||||||
channelUid = null
|
channelUid = null;
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.styleElement = document.createElement('style');
|
this.styleElement = document.createElement("style");
|
||||||
this.styleElement.innerHTML = `
|
this.styleElement.innerHTML = `
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
@@ -97,7 +99,7 @@ class UploadButtonElement extends HTMLElement {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
this.shadowRoot.appendChild(this.styleElement);
|
this.shadowRoot.appendChild(this.styleElement);
|
||||||
this.container = document.createElement('div');
|
this.container = document.createElement("div");
|
||||||
this.container.innerHTML = `
|
this.container.innerHTML = `
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<button class="upload-button">
|
<button class="upload-button">
|
||||||
@@ -107,16 +109,16 @@ class UploadButtonElement extends HTMLElement {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
this.shadowRoot.appendChild(this.container);
|
this.shadowRoot.appendChild(this.container);
|
||||||
this.channelUid = this.getAttribute('channel');
|
this.channelUid = this.getAttribute("channel");
|
||||||
this.uploadButton = this.container.querySelector('.upload-button');
|
this.uploadButton = this.container.querySelector(".upload-button");
|
||||||
this.fileInput = this.container.querySelector('.hidden-input');
|
this.fileInput = this.container.querySelector(".hidden-input");
|
||||||
this.uploadButton.addEventListener('click', () => {
|
this.uploadButton.addEventListener("click", () => {
|
||||||
this.fileInput.click();
|
this.fileInput.click();
|
||||||
});
|
});
|
||||||
this.fileInput.addEventListener('change', () => {
|
this.fileInput.addEventListener("change", () => {
|
||||||
this.uploadFiles();
|
this.uploadFiles();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('upload-button', UploadButtonElement);
|
customElements.define("upload-button", UploadButtonElement);
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
.user-list__item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.user-list__item-avatar {
|
||||||
|
margin-right: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.user-list__item-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.user-list__item-name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.user-list__item-text {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
.user-list__item-time {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
class UserList extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.users = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
set data(userArray) {
|
||||||
|
this.users = userArray;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
formatRelativeTime(timestamp) {
|
||||||
|
const now = new Date();
|
||||||
|
const msgTime = new Date(timestamp);
|
||||||
|
const diffMs = now - msgTime;
|
||||||
|
const minutes = Math.floor(diffMs / 60000);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${days} day${days > 1 ? "s" : ""} ago`;
|
||||||
|
} else if (hours > 0) {
|
||||||
|
return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${hours} hour${hours > 1 ? "s" : ""} ago`;
|
||||||
|
} else {
|
||||||
|
return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${minutes} min ago`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.innerHTML = "";
|
||||||
|
|
||||||
|
this.users.forEach((user) => {
|
||||||
|
const html = `
|
||||||
|
<div class="user-list__item"
|
||||||
|
data-uid="${user.uid}"
|
||||||
|
data-color="${user.color}"
|
||||||
|
data-user_nick="${user.nick}"
|
||||||
|
data-created_at="${user.created_at}"
|
||||||
|
data-user_uid="${user.user_uid}">
|
||||||
|
|
||||||
|
<a class="user-list__item-avatar" style="background-color: ${user.color}; color: black;" href="/user/${user.uid}.html">
|
||||||
|
<img width="40px" height="40px" src="/avatar/${user.uid}.svg" alt="${user.nick}">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="user-list__item-content">
|
||||||
|
<div class="user-list__item-name" style="color: ${user.color};">${user.nick}</div>
|
||||||
|
<div class="user-list__item-time" data-created_at="${user.last_ping}">
|
||||||
|
<a href="/user/${user.uid}.html">profile</a>
|
||||||
|
<a href="/channel/${user.uid}.html">dm</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.insertAdjacentHTML("beforeend", html);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("user-list", UserList);
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetWebSocketView:
|
||||||
|
def __init__(self):
|
||||||
|
self.ws = None
|
||||||
|
self.db = dataset.connect('sqlite:///snek.db')
|
||||||
|
self.setattr(self, "db", self.get)
|
||||||
|
self.setattr(self, "db", self.set)
|
||||||
|
)
|
||||||
|
super()
|
||||||
|
|
||||||
|
def format_result(self, result):
|
||||||
|
|
||||||
|
try:
|
||||||
|
return dict(result)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return [dict(row) for row in result]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def send_str(self, msg):
|
||||||
|
return await self.ws.send_str(msg)
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
returnl loads(dict(self.db['_kv'].get(key=key)['value']))
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
return self.db['_kv'].upsert({'key': key, 'value': json.dumps(value)}, ['key'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def handle(self, request):
|
||||||
|
ws = web.WebSocketResponse()
|
||||||
|
await ws.prepare(request)
|
||||||
|
self.ws = ws
|
||||||
|
|
||||||
|
async for msg in ws:
|
||||||
|
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||||
|
try:
|
||||||
|
data = json.loads(msg.data)
|
||||||
|
call_uid = data.get("call_uid")
|
||||||
|
method = data.get("method")
|
||||||
|
table_name = data.get("table")
|
||||||
|
args = data.get("args", {})
|
||||||
|
kwargs = data.get("kwargs", {})
|
||||||
|
|
||||||
|
|
||||||
|
function = getattr(self.db, method, None)
|
||||||
|
if table_name:
|
||||||
|
function = getattr(self.db[table_name], method, None)
|
||||||
|
|
||||||
|
print(method, table_name, args, kwargs,flush=True)
|
||||||
|
|
||||||
|
if function:
|
||||||
|
response = {}
|
||||||
|
try:
|
||||||
|
result = function(*args, **kwargs)
|
||||||
|
print(result)
|
||||||
|
response['result'] = self.format_result(result)
|
||||||
|
response["call_uid"] = call_uid
|
||||||
|
response["success"] = True
|
||||||
|
except Exception as e:
|
||||||
|
response["call_uid"] = call_uid
|
||||||
|
response["success"] = False
|
||||||
|
response["error"] = str(e)
|
||||||
|
response["traceback"] = traceback.format_exc()
|
||||||
|
|
||||||
|
if call_uid:
|
||||||
|
await self.send_str(json.dumps(response,default=str))
|
||||||
|
else:
|
||||||
|
await self.send_str(json.dumps({"status": "error", "error":"Method not found.","call_uid": call_uid}))
|
||||||
|
except Exception as e:
|
||||||
|
await self.send_str(json.dumps({"success": False,"call_uid": call_uid, "error": str(e), "error": str(e), "traceback": traceback.format_exc()},default=str))
|
||||||
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
|
print('ws connection closed with exception %s' % ws.exception())
|
||||||
|
|
||||||
|
return ws
|
||||||
|
|
||||||
|
class BroadCastSocketView:
|
||||||
|
def __init__(self):
|
||||||
|
self.ws = None
|
||||||
|
super()
|
||||||
|
|
||||||
|
def format_result(self, result):
|
||||||
|
|
||||||
|
try:
|
||||||
|
return dict(result)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return [dict(row) for row in result]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def send_str(self, msg):
|
||||||
|
return await self.ws.send_str(msg)
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
returnl loads(dict(self.db['_kv'].get(key=key)['value']))
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
return self.db['_kv'].upsert({'key': key, 'value': json.dumps(value)}, ['key'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def handle(self, request):
|
||||||
|
ws = web.WebSocketResponse()
|
||||||
|
await ws.prepare(request)
|
||||||
|
self.ws = ws
|
||||||
|
app = request.app
|
||||||
|
app['broadcast_clients'].append(ws)
|
||||||
|
|
||||||
|
async for msg in ws:
|
||||||
|
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||||
|
print(msg.data)
|
||||||
|
for client in app['broadcast_clients'] if not client == ws:
|
||||||
|
await client.send_str(msg.data)
|
||||||
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
|
print('ws connection closed with exception %s' % ws.exception())
|
||||||
|
app['broadcast_clients'].remove(ws)
|
||||||
|
return ws
|
||||||
|
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
view = DatasetWebSocketView()
|
||||||
|
app['broadcast_clients'] = []
|
||||||
|
app.router.add_get('/db', view.handle)
|
||||||
|
app.router.add_get('/broadcast', sync_view.handle)
|
||||||
|
|
||||||
+143
-59
@@ -1,67 +1,151 @@
|
|||||||
_C='delete'
|
import functools
|
||||||
_B='set'
|
import json
|
||||||
_A='get'
|
|
||||||
import functools,json
|
|
||||||
from snek.system import security
|
from snek.system import security
|
||||||
|
|
||||||
cache = functools.cache
|
cache = functools.cache
|
||||||
|
|
||||||
CACHE_MAX_ITEMS_DEFAULT = 5000
|
CACHE_MAX_ITEMS_DEFAULT = 5000
|
||||||
|
|
||||||
|
|
||||||
class Cache:
|
class Cache:
|
||||||
def __init__(A,app,max_items=CACHE_MAX_ITEMS_DEFAULT):A.app=app;A.cache={};A.max_items=max_items;A.stats={};A.lru=[];A.version=15505
|
def __init__(self, app, max_items=CACHE_MAX_ITEMS_DEFAULT):
|
||||||
async def get(A,args):
|
self.app = app
|
||||||
B=args;await A.update_stat(B,_A)
|
self.cache = {}
|
||||||
try:A.lru.pop(A.lru.index(B))
|
self.max_items = max_items
|
||||||
except:return
|
self.stats = {}
|
||||||
A.lru.insert(0,B)
|
self.enabled = False
|
||||||
while len(A.lru)>A.max_items:A.cache.pop(A.lru[-1]);A.lru.pop()
|
self.lru = []
|
||||||
return A.cache[B]
|
self.version = ((42 + 420 + 1984 + 1990 + 10 + 6 + 71 + 3004 + 7245) ^ 1337) + 4
|
||||||
async def get_stats(A):
|
|
||||||
C=[]
|
async def get(self, args):
|
||||||
for B in A.lru:C.append({'key':B,_B:A.stats[B][_B],_A:A.stats[B][_A],_C:A.stats[B][_C],'value':str(A.serialize(A.cache[B].record))})
|
if not self.enabled:
|
||||||
return C
|
return None
|
||||||
def serialize(C,obj):B=None;A=obj.copy();A.pop('created_at',B);A.pop('deleted_at',B);A.pop('email',B);A.pop('password',B);return A
|
await self.update_stat(args, "get")
|
||||||
async def update_stat(A,key,action):
|
try:
|
||||||
C=action;B=key
|
self.lru.pop(self.lru.index(args))
|
||||||
if B not in A.stats:A.stats[B]={_B:0,_A:0,_C:0}
|
except:
|
||||||
A.stats[B][C]=A.stats[B][C]+1
|
# print("Cache miss!", args, flush=True)
|
||||||
def json_default(B,value):
|
return None
|
||||||
A=value
|
self.lru.insert(0, args)
|
||||||
try:return json.dumps(A.__dict__,default=str)
|
while len(self.lru) > self.max_items:
|
||||||
except:return str(A)
|
self.cache.pop(self.lru[-1])
|
||||||
async def create_cache_key(A,args,kwargs):return await security.hash(json.dumps({'args':args,'kwargs':kwargs},sort_keys=True,default=A.json_default))
|
self.lru.pop()
|
||||||
async def set(A,args,result):
|
# print("Cache hit!", args, flush=True)
|
||||||
B=args;C=B not in A.cache;A.cache[B]=result;await A.update_stat(B,_B)
|
return self.cache[args]
|
||||||
try:A.lru.pop(A.lru.index(B))
|
|
||||||
except(ValueError,IndexError):pass
|
async def get_stats(self):
|
||||||
A.lru.insert(0,B)
|
all_ = []
|
||||||
while len(A.lru)>A.max_items:A.cache.pop(A.lru[-1]);A.lru.pop()
|
for key in self.lru:
|
||||||
if C:A.version+=1
|
all_.append(
|
||||||
async def delete(A,args):
|
{
|
||||||
B=args;await A.update_stat(B,_C)
|
"key": key,
|
||||||
if B in A.cache:
|
"set": self.stats[key]["set"],
|
||||||
try:A.lru.pop(A.lru.index(B))
|
"get": self.stats[key]["get"],
|
||||||
except IndexError:pass
|
"delete": self.stats[key]["delete"],
|
||||||
del A.cache[B]
|
"value": str(self.serialize(self.cache[key].record)),
|
||||||
def async_cache(A,func):
|
}
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
is_new = args not in self.cache
|
||||||
|
self.cache[args] = result
|
||||||
|
await self.update_stat(args, "set")
|
||||||
|
try:
|
||||||
|
self.lru.pop(self.lru.index(args))
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
self.lru.insert(0, args)
|
||||||
|
|
||||||
|
while len(self.lru) > self.max_items:
|
||||||
|
self.cache.pop(self.lru[-1])
|
||||||
|
self.lru.pop()
|
||||||
|
|
||||||
|
if is_new:
|
||||||
|
self.version += 1
|
||||||
|
# print(f"Cache store! {len(self.lru)} items. New version:", self.version, flush=True)
|
||||||
|
|
||||||
|
async def delete(self, args):
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
await self.update_stat(args, "delete")
|
||||||
|
if args in self.cache:
|
||||||
|
try:
|
||||||
|
self.lru.pop(self.lru.index(args))
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
del self.cache[args]
|
||||||
|
|
||||||
|
def async_cache(self, func):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
async def B(*B,**C):
|
async def wrapper(*args, **kwargs):
|
||||||
D=await A.create_cache_key(B,C);E=await A.get(D)
|
cache_key = await self.create_cache_key(args, kwargs)
|
||||||
if E:return E
|
cached = await self.get(cache_key)
|
||||||
F=await func(*B,**C);await A.set(D,F);return F
|
if cached:
|
||||||
return B
|
return cached
|
||||||
def async_delete_cache(A,func):
|
result = await func(*args, **kwargs)
|
||||||
|
await self.set(cache_key, result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def async_delete_cache(self, func):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
async def B(*C,**D):
|
async def wrapper(*args, **kwargs):
|
||||||
B=await A.create_cache_key(C,D)
|
cache_key = await self.create_cache_key(args, kwargs)
|
||||||
if B in A.cache:
|
if cache_key in self.cache:
|
||||||
try:A.lru.pop(A.lru.index(B))
|
try:
|
||||||
except IndexError:pass
|
self.lru.pop(self.lru.index(cache_key))
|
||||||
del A.cache[B]
|
except IndexError:
|
||||||
return await func(*C,**D)
|
pass
|
||||||
return B
|
del self.cache[cache_key]
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def async_cache(func):
|
def async_cache(func):
|
||||||
B={}
|
cache = {}
|
||||||
|
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
async def A(*A):
|
async def wrapper(*args):
|
||||||
if A in B:return B[A]
|
if args in cache:
|
||||||
C=await func(*A);B[A]=C;return C
|
return cache[args]
|
||||||
return A
|
result = await func(*args)
|
||||||
|
cache[args] = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|||||||
+111
-23
@@ -1,32 +1,120 @@
|
|||||||
_B='fields'
|
# Written by retoor@molodetz.nl
|
||||||
_A=None
|
|
||||||
|
# This code defines a framework for handling HTML elements as Python objects, including specific classes for HTML, form input, and form button elements. It offers methods to convert these elements to JSON, manipulate them, and validate form data.
|
||||||
|
|
||||||
|
# This code uses the `snek.system.model` library for managing model fields.
|
||||||
|
|
||||||
|
# MIT License
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
from snek.system import model
|
from snek.system import model
|
||||||
|
|
||||||
|
|
||||||
class HTMLElement(model.ModelField):
|
class HTMLElement(model.ModelField):
|
||||||
def __init__(A,id=_A,tag='div',name=_A,html=_A,class_name=_A,text=_A,*B,**C):A.tag=tag;A.text=text;A.id=id;A.class_name=class_name or name;A.html=html;super().__init__(*B,name=name,**C)
|
def __init__(
|
||||||
async def to_json(B):A=await super().to_json();A['text']=B.text;A['id']=B.id;A['html']=B.html;A['class_name']=B.class_name;A['tag']=B.tag;return A
|
self,
|
||||||
class FormElement(HTMLElement):0
|
id=None,
|
||||||
|
tag="div",
|
||||||
|
name=None,
|
||||||
|
html=None,
|
||||||
|
class_name=None,
|
||||||
|
text=None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
self.tag = tag
|
||||||
|
self.text = text
|
||||||
|
self.id = id
|
||||||
|
self.class_name = class_name or name
|
||||||
|
self.html = html
|
||||||
|
super().__init__(name=name, *args, **kwargs)
|
||||||
|
|
||||||
|
async def to_json(self):
|
||||||
|
result = await super().to_json()
|
||||||
|
result["text"] = self.text
|
||||||
|
result["id"] = self.id
|
||||||
|
result["html"] = self.html
|
||||||
|
result["class_name"] = self.class_name
|
||||||
|
result["tag"] = self.tag
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class FormElement(HTMLElement):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FormInputElement(FormElement):
|
class FormInputElement(FormElement):
|
||||||
def __init__(A,type='text',place_holder=_A,*B,**C):super().__init__(*B,tag='input',**C);A.place_holder=place_holder;A.type=type
|
def __init__(self, type="text", place_holder=None, *args, **kwargs):
|
||||||
async def to_json(B):A=await super().to_json();A['place_holder']=B.place_holder;A['type']=B.type;return A
|
super().__init__(tag="input", *args, **kwargs)
|
||||||
|
self.place_holder = place_holder
|
||||||
|
self.type = type
|
||||||
|
|
||||||
|
async def to_json(self):
|
||||||
|
data = await super().to_json()
|
||||||
|
data["place_holder"] = self.place_holder
|
||||||
|
data["type"] = self.type
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class FormButtonElement(FormElement):
|
class FormButtonElement(FormElement):
|
||||||
def __init__(C,tag='button',*A,**B):super().__init__(*A,tag=tag,**B)
|
def __init__(self, tag="button", *args, **kwargs):
|
||||||
|
super().__init__(tag=tag, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Form(model.BaseModel):
|
class Form(model.BaseModel):
|
||||||
@property
|
@property
|
||||||
def html_elements(self):return[A for A in self.fields if isinstance(A,HTMLElement)]
|
def html_elements(self):
|
||||||
def set_user_data(A,data):return super().set_user_data(data.get(_B))
|
return [element for element in self.fields if isinstance(element, HTMLElement)]
|
||||||
async def to_json(D,encode=False):
|
|
||||||
B='is_valid';E=await super().to_json();C={}
|
def set_user_data(self, data):
|
||||||
for A in E.keys():
|
return super().set_user_data(data.get("fields"))
|
||||||
if A==B:continue
|
|
||||||
F=getattr(D,A)
|
async def to_json(self, encode=False):
|
||||||
if isinstance(F,HTMLElement):
|
elements = await super().to_json()
|
||||||
try:C[A]=E[A]
|
html_elements = {}
|
||||||
except KeyError:pass
|
for element in elements.keys():
|
||||||
G=all(A[B]for A in C.values());return{_B:C,B:G,'errors':await D.errors}
|
if element == "is_valid":
|
||||||
|
# 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
|
@property
|
||||||
async def errors(self):
|
async def errors(self):
|
||||||
A=[]
|
result = []
|
||||||
for B in self.html_elements:A+=await B.errors
|
for field in self.html_elements:
|
||||||
return A
|
result += await field.errors
|
||||||
|
return result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
async def is_valid(self):return False
|
async def is_valid(self):
|
||||||
|
# This is not good, but timebox to resolve issue exceeded.
|
||||||
|
return False
|
||||||
|
|||||||
+102
-36
@@ -1,44 +1,110 @@
|
|||||||
import asyncio,pathlib,uuid,zlib
|
# Written by retoor@molodetz.nl
|
||||||
|
|
||||||
|
# This script enables downloading, processing, and caching web content, including taking website screenshots and repairing links in HTML content.
|
||||||
|
|
||||||
|
# Imports used: aiohttp, aiohttp.web for creating web servers and handling async requests; app.cache for caching utilities; BeautifulSoup from bs4 for HTML parsing; imgkit for creating screenshots.
|
||||||
|
|
||||||
|
# The MIT License (MIT)
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import pathlib
|
||||||
|
import uuid
|
||||||
|
import zlib
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
import aiohttp,imgkit
|
|
||||||
|
import aiohttp
|
||||||
|
import imgkit
|
||||||
from app.cache import time_cache_async
|
from app.cache import time_cache_async
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
||||||
async def crc32(data):
|
async def crc32(data):
|
||||||
A=data
|
try:
|
||||||
try:A=A.encode()
|
data = data.encode()
|
||||||
except:pass
|
except:
|
||||||
return'crc32'+str(zlib.crc32(A))
|
pass
|
||||||
async def get_file(name,suffix='.cache'):
|
return "crc32" + str(zlib.crc32(data))
|
||||||
A=name;A=await crc32(A);B=pathlib.Path('.').joinpath('cache')
|
|
||||||
if not B.exists():B.mkdir(parents=True,exist_ok=True)
|
|
||||||
return B.joinpath(A+suffix)
|
async def get_file(name, suffix=".cache"):
|
||||||
async def public_touch(name=None):A=pathlib.Path('.').joinpath(str(uuid.uuid4())+name);A.open('wb').close();return A
|
name = await crc32(name)
|
||||||
|
path = pathlib.Path(".").joinpath("cache")
|
||||||
|
if not path.exists():
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
return path.joinpath(name + suffix)
|
||||||
|
|
||||||
|
|
||||||
|
async def public_touch(name=None):
|
||||||
|
path = pathlib.Path(".").joinpath(str(uuid.uuid4()) + name)
|
||||||
|
path.open("wb").close()
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
async def create_site_photo(url):
|
async def create_site_photo(url):
|
||||||
A=url;C=asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
if not A.startswith('https'):A='https://'+A
|
if not url.startswith("https"):
|
||||||
B=await get_file('site-screenshot-'+A,'.png')
|
url = "https://" + url
|
||||||
if B.exists():return B
|
output_path = await get_file("site-screenshot-" + url, ".png")
|
||||||
B.touch()
|
|
||||||
def D():imgkit.from_url(A,B.absolute());return B
|
if output_path.exists():
|
||||||
return await C.run_in_executor(None,D)
|
return output_path
|
||||||
|
output_path.touch()
|
||||||
|
|
||||||
|
def make_photo():
|
||||||
|
imgkit.from_url(url, output_path.absolute())
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
return await loop.run_in_executor(None, make_photo)
|
||||||
|
|
||||||
|
|
||||||
async def repair_links(base_url, html_content):
|
async def repair_links(base_url, html_content):
|
||||||
D='http';E=base_url;B='src';C='href';F=BeautifulSoup(html_content,'html.parser')
|
soup = BeautifulSoup(html_content, "html.parser")
|
||||||
for A in F.find_all(['a','img','link']):
|
for tag in soup.find_all(["a", "img", "link"]):
|
||||||
if A.has_attr(C)and not A[C].startswith(D):A[C]=urljoin(E,A[C])
|
if tag.has_attr("href") and not tag["href"].startswith("http"):
|
||||||
if A.has_attr(B)and not A[B].startswith(D):A[B]=urljoin(E,A[B])
|
tag["href"] = urljoin(base_url, tag["href"])
|
||||||
return F.prettify()
|
if tag.has_attr("src") and not tag["src"].startswith("http"):
|
||||||
async def is_html_content(content):
|
tag["src"] = urljoin(base_url, tag["src"])
|
||||||
B=False;A=content
|
return soup.prettify()
|
||||||
if not A:return B
|
|
||||||
try:A=A.decode(errors='ignore')
|
|
||||||
except:pass
|
async def is_html_content(content: bytes):
|
||||||
C=['<html','<img','<p','<span','<div'];A=A.lower()
|
if not content:
|
||||||
for D in C:
|
return False
|
||||||
if D in A:return True
|
try:
|
||||||
return B
|
content = content.decode(errors="ignore")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
marks = ["<html", "<img", "<p", "<span", "<div"]
|
||||||
|
content = content.lower()
|
||||||
|
for mark in marks:
|
||||||
|
if mark in content:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@time_cache_async(120)
|
@time_cache_async(120)
|
||||||
async def get(url):
|
async def get(url):
|
||||||
async with aiohttp.ClientSession()as B:
|
async with aiohttp.ClientSession() as session:
|
||||||
C=await B.get(url);A=await C.text()
|
response = await session.get(url)
|
||||||
if await is_html_content(A):A=(await repair_links(url,A)).encode()
|
content = await response.text()
|
||||||
return A
|
if await is_html_content(content):
|
||||||
|
content = (await repair_links(url, content)).encode()
|
||||||
|
return content
|
||||||
|
|||||||
+72
-31
@@ -1,37 +1,78 @@
|
|||||||
_A='uid'
|
|
||||||
DEFAULT_LIMIT = 30
|
DEFAULT_LIMIT = 30
|
||||||
import typing
|
import typing
|
||||||
|
import asyncio
|
||||||
from snek.system.model import BaseModel
|
from snek.system.model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class BaseMapper:
|
class BaseMapper:
|
||||||
model_class:BaseModel=None;default_limit:int=DEFAULT_LIMIT;table_name:str=None
|
|
||||||
def __init__(A,app):A.app=app;A.default_limit=A.__class__.default_limit
|
model_class: BaseModel = None
|
||||||
|
default_limit: int = DEFAULT_LIMIT
|
||||||
|
table_name: str = None
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
self.default_limit = self.__class__.default_limit
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def db(self):return self.app.db
|
def db(self):
|
||||||
async def new(A):return A.model_class(mapper=A,app=A.app)
|
return self.app.db
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self):return self.db[self.table_name]
|
def loop(self):
|
||||||
async def get(B,uid=None,**C):
|
return asyncio.get_event_loop()
|
||||||
if uid:C[_A]=uid
|
|
||||||
A=B.table.find_one(**C)
|
async def run_in_executor(self, func, *args, **kwargs):
|
||||||
if not A:return
|
return await self.loop.run_in_executor(None, lambda: func(*args, **kwargs))
|
||||||
A=dict(A);D=await B.new()
|
|
||||||
for(E,F)in A.items():D[E]=F
|
async def new(self):
|
||||||
return D;return await B.model_class.from_record(mapper=B,record=A)
|
return self.model_class(mapper=self, app=self.app)
|
||||||
async def exists(A,**B):return A.table.exists(**B)
|
|
||||||
async def count(A,**B):return A.table.count(**B)
|
@property
|
||||||
async def save(B,model):
|
def table(self):
|
||||||
A=model
|
return self.db[self.table_name]
|
||||||
if not A.record.get(_A):raise Exception(f"Attempt to save without uid: {A.record}.")
|
|
||||||
A.updated_at.update();return B.table.upsert(A.record,[_A])
|
async def get(self, uid: str = None, **kwargs) -> BaseModel:
|
||||||
async def find(A,**B):
|
if uid:
|
||||||
C='_limit'
|
kwargs["uid"] = uid
|
||||||
if not B.get(C):B[C]=A.default_limit
|
|
||||||
for E in A.table.find(**B):
|
record = await self.run_in_executor(self.table.find_one,**kwargs)
|
||||||
D=await A.new()
|
if not record:
|
||||||
for(F,G)in E.items():D[F]=G
|
return None
|
||||||
yield D
|
record = dict(record)
|
||||||
async def query(A,sql,*B):
|
model = await self.new()
|
||||||
for C in A.db.query(sql,*B):yield dict(C)
|
for key, value in record.items():
|
||||||
async def delete(B,**A):
|
model[key] = value
|
||||||
if not A or not isinstance(A,dict):raise Exception("Can't execute delete with no filter.")
|
return model
|
||||||
return B.table.delete(**A)
|
return await self.model_class.from_record(mapper=self, record=record)
|
||||||
|
|
||||||
|
async def exists(self, **kwargs):
|
||||||
|
return await self.run_in_executor(self.table.exists,**kwargs)
|
||||||
|
|
||||||
|
async def count(self, **kwargs) -> int:
|
||||||
|
return await self.run_in_executor(self.table.count, **kwargs)
|
||||||
|
|
||||||
|
async def save(self, model: BaseModel) -> bool:
|
||||||
|
if not model.record.get("uid"):
|
||||||
|
raise Exception(f"Attempt to save without uid: {model.record}.")
|
||||||
|
model.updated_at.update()
|
||||||
|
return await self.run_in_executor(self.table.upsert, model.record, ["uid"])
|
||||||
|
|
||||||
|
async def find(self, **kwargs) -> typing.AsyncGenerator:
|
||||||
|
if not kwargs.get("_limit"):
|
||||||
|
kwargs["_limit"] = self.default_limit
|
||||||
|
for record in await self.run_in_executor(self.table.find, **kwargs):
|
||||||
|
model = await self.new()
|
||||||
|
for key, value in record.items():
|
||||||
|
model[key] = value
|
||||||
|
yield model
|
||||||
|
|
||||||
|
async def query(self, sql, *args):
|
||||||
|
for record in await self.run_in_executor(self.db.query,sql, *args):
|
||||||
|
yield dict(record)
|
||||||
|
|
||||||
|
async def delete(self, **kwargs) -> int:
|
||||||
|
if not kwargs or not isinstance(kwargs, dict):
|
||||||
|
raise Exception("Can't execute delete with no filter.")
|
||||||
|
return await self.run_in_executor(self.table.delete, **kwargs)
|
||||||
|
|||||||
+88
-23
@@ -1,35 +1,100 @@
|
|||||||
_A=True
|
# Original source: https://brandonjay.dev/posts/2021/render-markdown-html-in-python-with-jinja2
|
||||||
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from app.cache import time_cache_async
|
from app.cache import time_cache_async
|
||||||
from mistune import HTMLRenderer, Markdown
|
from mistune import HTMLRenderer, Markdown
|
||||||
|
from mistune.plugins.formatting import strikethrough
|
||||||
|
from mistune.plugins.spoiler import spoiler
|
||||||
|
from mistune.plugins.url import url
|
||||||
from pygments import highlight
|
from pygments import highlight
|
||||||
from pygments.formatters import html
|
from pygments.formatters import html
|
||||||
from pygments.lexers import get_lexer_by_name
|
from pygments.lexers import get_lexer_by_name
|
||||||
|
|
||||||
|
|
||||||
class MarkdownRenderer(HTMLRenderer):
|
class MarkdownRenderer(HTMLRenderer):
|
||||||
_allow_harmful_protocols=_A
|
|
||||||
def __init__(A,app,template):A.template=template;A.app=app;A.env=A.app.jinja2_env;B=html.HtmlFormatter();A.env.globals['highlight_styles']=B.get_style_defs()
|
_allow_harmful_protocols = True
|
||||||
def _escape(A,str):return str
|
|
||||||
def get_lexer(A,lang,default='bash'):
|
def __init__(self, app, template):
|
||||||
try:return get_lexer_by_name(lang,stripall=_A)
|
super().__init__(False, True)
|
||||||
except:return get_lexer_by_name(default,stripall=_A)
|
|
||||||
def block_code(B,code,lang=None,info=None):
|
self.template = template
|
||||||
A=lang
|
|
||||||
if not A:A=info
|
self.app = app
|
||||||
if not A:A='bash'
|
self.env = self.app.jinja2_env
|
||||||
C=B.get_lexer(A);D=html.HtmlFormatter(lineseparator='<br>');E=highlight(code,C,D);return E
|
formatter = html.HtmlFormatter()
|
||||||
def render(A):B=A.app.template_path.joinpath(A.template).read_text();C=MarkdownRenderer(A.app,A.template);D=Markdown(renderer=C);return D(B)
|
self.env.globals["highlight_styles"] = formatter.get_style_defs()
|
||||||
def render_markdown_sync(app,markdown_string):A=MarkdownRenderer(app,None);B=Markdown(renderer=A);return B(markdown_string)
|
|
||||||
|
def _escape(self, str):
|
||||||
|
return str ##escape(str)
|
||||||
|
|
||||||
|
def get_lexer(self, lang, default="bash"):
|
||||||
|
try:
|
||||||
|
return get_lexer_by_name(lang, stripall=True)
|
||||||
|
except:
|
||||||
|
return get_lexer_by_name(default, stripall=True)
|
||||||
|
|
||||||
|
def block_code(self, code, lang=None, info=None):
|
||||||
|
if not lang:
|
||||||
|
lang = info
|
||||||
|
if not lang:
|
||||||
|
lang = "bash"
|
||||||
|
lexer = self.get_lexer(lang)
|
||||||
|
formatter = html.HtmlFormatter(lineseparator="<br>")
|
||||||
|
result = highlight(code, lexer, formatter)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
markdown_string = self.app.template_path.joinpath(self.template).read_text()
|
||||||
|
renderer = MarkdownRenderer(self.app, self.template)
|
||||||
|
markdown = Markdown(renderer=renderer)
|
||||||
|
return markdown(markdown_string)
|
||||||
|
|
||||||
|
# def image(self, text: str, url: str, title: Optional[str] = None) -> str:
|
||||||
|
# src = self.safe_url(url)
|
||||||
|
# alt = escape(striptags(text))
|
||||||
|
# s = '<picture><srcset srcset="' + src +'" /><img src="' + src + '" alt="' + alt + '"'
|
||||||
|
# if title:
|
||||||
|
# s += ' title="' + safe_entity(title) + '"'
|
||||||
|
# return s + ' /></picture>'
|
||||||
|
|
||||||
|
|
||||||
|
def render_markdown_sync(app, markdown_string):
|
||||||
|
renderer = MarkdownRenderer(app, None)
|
||||||
|
markdown = Markdown(renderer=renderer, plugins=[url, strikethrough, spoiler])
|
||||||
|
return markdown(markdown_string)
|
||||||
|
|
||||||
|
|
||||||
@time_cache_async(120)
|
@time_cache_async(120)
|
||||||
async def render_markdown(app,markdown_string):return render_markdown_sync(app,markdown_string)
|
async def render_markdown(app, markdown_string):
|
||||||
|
return render_markdown_sync(app, markdown_string)
|
||||||
|
|
||||||
|
|
||||||
from jinja2 import TemplateSyntaxError, nodes
|
from jinja2 import TemplateSyntaxError, nodes
|
||||||
from jinja2.ext import Extension
|
from jinja2.ext import Extension
|
||||||
from jinja2.nodes import Const
|
from jinja2.nodes import Const
|
||||||
|
|
||||||
|
|
||||||
|
# Source: https://ron.sh/how-to-write-a-jinja2-extension/
|
||||||
class MarkdownExtension(Extension):
|
class MarkdownExtension(Extension):
|
||||||
tags={'markdown'}
|
tags = {"markdown"}
|
||||||
def __init__(A,environment):B=environment;A.app=SimpleNamespace(jinja2_env=B);super(MarkdownExtension,A).__init__(B)
|
|
||||||
def parse(D,parser):
|
def __init__(self, environment):
|
||||||
A=parser;E=next(A.stream).lineno;B=[Const('')];C=''
|
self.app = SimpleNamespace(jinja2_env=environment)
|
||||||
try:B=[A.parse_expression()]
|
super(MarkdownExtension, self).__init__(environment)
|
||||||
except TemplateSyntaxError:C=A.parse_statements(['name:endmarkdown'],drop_needle=_A)
|
|
||||||
return nodes.CallBlock(D.call_method('_to_html',B),[],[],C).set_lineno(E)
|
def parse(self, parser):
|
||||||
def _to_html(A,md_file,caller):return render_markdown_sync(A.app,caller())
|
line_number = next(parser.stream).lineno
|
||||||
|
md_file = [Const("")]
|
||||||
|
body = ""
|
||||||
|
try:
|
||||||
|
md_file = [parser.parse_expression()]
|
||||||
|
except TemplateSyntaxError:
|
||||||
|
body = parser.parse_statements(["name:endmarkdown"], drop_needle=True)
|
||||||
|
return nodes.CallBlock(
|
||||||
|
self.call_method("_to_html", md_file), [], [], body
|
||||||
|
).set_lineno(line_number)
|
||||||
|
|
||||||
|
def _to_html(self, md_file, caller):
|
||||||
|
return render_markdown_sync(self.app, caller())
|
||||||
|
|||||||
@@ -1,21 +1,53 @@
|
|||||||
_D='Access-Control-Allow-Credentials'
|
# Written by retoor@molodetz.nl
|
||||||
_C='Access-Control-Allow-Headers'
|
|
||||||
_B='Access-Control-Allow-Methods'
|
# This code provides middleware functions for an aiohttp server to manage and modify CORS (Cross-Origin Resource Sharing) headers.
|
||||||
_A='Access-Control-Allow-Origin'
|
|
||||||
|
# Imports from 'aiohttp' library are used to create middleware; they are not part of Python's standard library.
|
||||||
|
|
||||||
|
# MIT License: This code is distributed under the MIT License.
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def no_cors_middleware(request,handler):A=await handler(request);A.headers.pop(_A,None);return A
|
async def no_cors_middleware(request, handler):
|
||||||
|
response = await handler(request)
|
||||||
|
response.headers.pop("Access-Control-Allow-Origin", None)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def cors_allow_middleware(request,handler):A=await handler(request);A.headers[_A]='*';A.headers[_B]='GET, POST, OPTIONS, PUT, DELETE, MOVE, COPY, HEAD, LOCK, UNLOCK, PATCH, PROPFIND';A.headers[_C]='*';A.headers[_D]='true';return A
|
async def cors_allow_middleware(request, handler):
|
||||||
|
response = await handler(request)
|
||||||
|
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
response.headers["Access-Control-Allow-Methods"] = (
|
||||||
|
"GET, POST, OPTIONS, PUT, DELETE, MOVE, COPY, HEAD, LOCK, UNLOCK, PATCH, PROPFIND"
|
||||||
|
)
|
||||||
|
response.headers["Access-Control-Allow-Headers"] = "*"
|
||||||
|
response.headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def auth_middleware(request, handler):
|
async def auth_middleware(request, handler):
|
||||||
B='uid';C='user';A=request;A[C]=None
|
request["user"] = None
|
||||||
if A.session.get(B)and A.session.get('logged_in'):A[C]=await A.app.services.user.get(uid=A.app.session.get(B))
|
if request.session.get("uid") and request.session.get("logged_in"):
|
||||||
return await handler(A)
|
request["user"] = await request.app.services.user.get(
|
||||||
|
uid=request.app.session.get("uid")
|
||||||
|
)
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def cors_middleware(request, handler):
|
async def cors_middleware(request, handler):
|
||||||
C='Allow';D=handler;B=request
|
if request.headers.get("Allow"):
|
||||||
if B.headers.get(C):return await D(B)
|
return await handler(request)
|
||||||
A=await D(B)
|
|
||||||
if B.headers.get(C):return A
|
response = await handler(request)
|
||||||
A.headers[_A]='*';A.headers[_B]='GET, POST, PUT, DELETE, OPTIONS';A.headers[_C]='*';A.headers[_D]='true';return A
|
if request.headers.get("Allow"):
|
||||||
|
return response
|
||||||
|
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
|
||||||
|
response.headers["Access-Control-Allow-Headers"] = "*"
|
||||||
|
response.headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
|
return response
|
||||||
|
|||||||
+343
-105
@@ -1,139 +1,377 @@
|
|||||||
_I='deleted_at'
|
# Written by retoor@molodetz.nl
|
||||||
_H='updated_at'
|
|
||||||
_G='created_at'
|
# The script defines a flexible validation and field management system for models, with capabilities for setting attributes, validation, error handling, and JSON conversion. It includes classes for managing various field types with specific properties such as UUID, timestamps for creation and updates, and custom validation rules.
|
||||||
_F='is_valid'
|
|
||||||
_E='name'
|
# This script utilizes external Python libraries such as 're' for regex operations, 'uuid' for generating unique identifiers, and 'json' for data interchange. The 'datetime' and 'timezone' modules from the Python standard library are used for date and time operations. 'OrderedDict' from 'collections' provides enhanced dictionary capabilities, and 'copy' allows deep copying of objects.
|
||||||
_D=False
|
|
||||||
_C='value'
|
# MIT License
|
||||||
_B=True
|
#
|
||||||
_A=None
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
import copy,json,re,uuid
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
TIMESTAMP_REGEX='^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}\\+\\d{2}:\\d{2}$'
|
|
||||||
def now():return str(datetime.now(timezone.utc))
|
TIMESTAMP_REGEX = r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}\+\d{2}:\d{2}$"
|
||||||
def add_attrs(**A):
|
|
||||||
def B(func):
|
|
||||||
for(B,C)in A.items():setattr(func,B,C)
|
def now():
|
||||||
|
return 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 func
|
||||||
return B
|
|
||||||
def validate_attrs(required=_D,min_length=_A,max_length=_A,regex=_A,**A):
|
return decorator
|
||||||
def B(func):return add_attrs(required=required,min_length=min_length,max_length=max_length,regex=regex,**A)(func)
|
|
||||||
|
|
||||||
|
def validate_attrs(
|
||||||
|
required=False, min_length=None, max_length=None, regex=None, **kwargs
|
||||||
|
):
|
||||||
|
def decorator(func):
|
||||||
|
return add_attrs(
|
||||||
|
required=required,
|
||||||
|
min_length=min_length,
|
||||||
|
max_length=max_length,
|
||||||
|
regex=regex,
|
||||||
|
**kwargs,
|
||||||
|
)(func)
|
||||||
|
|
||||||
|
|
||||||
class Validator:
|
class Validator:
|
||||||
_index = 0
|
_index = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self):return self._value
|
def value(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
@value.setter
|
@value.setter
|
||||||
def value(self,val):self._value=json.loads(json.dumps(val,default=str))
|
def value(self, val):
|
||||||
|
self._value = json.loads(json.dumps(val, default=str))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def initial_value(self):return self.value
|
def initial_value(self):
|
||||||
def custom_validation(A):return _B
|
return self.value
|
||||||
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)
|
|
||||||
|
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
|
@property
|
||||||
async def errors(self):
|
async def errors(self):
|
||||||
A=self;B=[]
|
error_list = []
|
||||||
if A.value is _A and A.required:B.append('Field is required.');return B
|
if self.value is None and self.required:
|
||||||
if A.value is _A:return B
|
error_list.append("Field is required.")
|
||||||
if A.kind in[int,float]:
|
return error_list
|
||||||
if A.min_num is not _A and A.value<A.min_num:B.append(f"Field should be minimal {A.min_num}.")
|
|
||||||
if A.max_num is not _A and A.value>A.max_num:B.append(f"Field should be maximal {A.max_num}.")
|
if self.value is None:
|
||||||
if A.min_length is not _A and len(A.value)<A.min_length:B.append(f"Field should be minimal {A.min_length} characters long.")
|
return error_list
|
||||||
if A.max_length is not _A and len(A.value)>A.max_length:B.append(f"Field should be maximal {A.max_length} characters long.")
|
|
||||||
if A.regex and A.value and not re.match(A.regex,A.value):B.append('Invalid value.')
|
if self.kind in [int, float]:
|
||||||
if A.kind and not isinstance(A.value,A.kind):B.append(f"Invalid kind. It is supposed to be {A.kind}.")
|
if self.min_num is not None and self.value < self.min_num:
|
||||||
return B
|
error_list.append(f"Field should be minimal {self.min_num}.")
|
||||||
async def validate(B):
|
if self.max_num is not None and self.value > self.max_num:
|
||||||
A=await B.errors
|
error_list.append(f"Field should be maximal {self.max_num}.")
|
||||||
if A:raise ValueError(f"Errors: {A}.")
|
if self.min_length is not None and len(self.value) < self.min_length:
|
||||||
return _B
|
error_list.append(
|
||||||
def __repr__(A):return str(A.to_json())
|
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
|
@property
|
||||||
async def is_valid(self):
|
async def is_valid(self):
|
||||||
try:await self.validate();return _B
|
try:
|
||||||
except ValueError:return _D
|
await self.validate()
|
||||||
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}
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def to_json(self):
|
||||||
|
errors = await self.errors
|
||||||
|
is_valid = await self.is_valid
|
||||||
|
return {
|
||||||
|
"required": self.required,
|
||||||
|
"min_num": self.min_num,
|
||||||
|
"max_num": self.max_num,
|
||||||
|
"min_length": self.min_length,
|
||||||
|
"max_length": self.max_length,
|
||||||
|
"regex": self.regex,
|
||||||
|
"value": self.value,
|
||||||
|
"kind": str(self.kind),
|
||||||
|
"help_text": self.help_text,
|
||||||
|
"errors": errors,
|
||||||
|
"is_valid": is_valid,
|
||||||
|
"index": self.index,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ModelField(Validator):
|
class ModelField(Validator):
|
||||||
|
|
||||||
index = 1
|
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
|
def __init__(self, name=None, save=True, *args, **kwargs):
|
||||||
|
self.name = name
|
||||||
|
self.save = save
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
async def to_json(self):
|
||||||
|
result = await super().to_json()
|
||||||
|
result["name"] = self.name
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class CreatedField(ModelField):
|
class CreatedField(ModelField):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def initial_value(self):return now()
|
def initial_value(self):
|
||||||
def update(A):
|
return now()
|
||||||
if not A.value:A.value=now()
|
|
||||||
|
def update(self):
|
||||||
|
if not self.value:
|
||||||
|
self.value = now()
|
||||||
|
|
||||||
|
|
||||||
class UpdatedField(ModelField):
|
class UpdatedField(ModelField):
|
||||||
def update(A):A.value=now()
|
|
||||||
|
def update(self):
|
||||||
|
self.value = now()
|
||||||
|
|
||||||
|
|
||||||
class DeletedField(ModelField):
|
class DeletedField(ModelField):
|
||||||
def update(A):A.value=now()
|
|
||||||
|
def update(self):
|
||||||
|
self.value = now()
|
||||||
|
|
||||||
|
|
||||||
class UUIDField(ModelField):
|
class UUIDField(ModelField):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self):return str(self._value)
|
def value(self):
|
||||||
|
return str(self._value)
|
||||||
|
|
||||||
@value.setter
|
@value.setter
|
||||||
def value(self,val):self._value=str(val)
|
def value(self, val):
|
||||||
|
self._value = str(val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def initial_value(self):return str(uuid.uuid4())
|
def initial_value(self):
|
||||||
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
class BaseModel:
|
class BaseModel:
|
||||||
uid=UUIDField(name='uid',required=_B);created_at=CreatedField(name=_G,required=_B,regex=TIMESTAMP_REGEX,place_holder='Created at');updated_at=UpdatedField(name=_H,regex=TIMESTAMP_REGEX,place_holder='Updated at');deleted_at=DeletedField(name=_I,regex=TIMESTAMP_REGEX,place_holder='Deleted at')
|
|
||||||
|
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
|
@classmethod
|
||||||
async def from_record(B,record,mapper):A=B();A.mapper=mapper;A.record=record;return A
|
async def from_record(cls, record, mapper):
|
||||||
|
model = cls()
|
||||||
|
model.mapper = mapper
|
||||||
|
model.record = record
|
||||||
|
return model
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mapper(self):return self._mapper
|
def mapper(self):
|
||||||
|
return self._mapper
|
||||||
|
|
||||||
@mapper.setter
|
@mapper.setter
|
||||||
def mapper(self,value):self._mapper=value
|
def mapper(self, value):
|
||||||
|
self._mapper = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def record(self):return{A:B.value for(A,B)in self.fields.items()}
|
def record(self):
|
||||||
|
return {key: field.value for key, field in self.fields.items()}
|
||||||
|
|
||||||
@record.setter
|
@record.setter
|
||||||
def record(self, val):
|
def record(self, val):
|
||||||
A=self
|
for key, value in val.items():
|
||||||
for(B,C)in val.items():
|
field = self.fields.get(key)
|
||||||
D=A.fields.get(B)
|
if not field:
|
||||||
if not D:continue
|
continue
|
||||||
A[B]=C
|
self[key] = value
|
||||||
return A
|
return self
|
||||||
def __init__(A,*F,**C):
|
|
||||||
D='app';A._mapper=C.get('mapper');A.app=C.get(D);A.fields={}
|
def __init__(self, *args, **kwargs):
|
||||||
for B in dir(A.__class__):
|
self._mapper = kwargs.get("mapper")
|
||||||
E=getattr(A.__class__,B)
|
self.app = kwargs.get("app")
|
||||||
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)
|
self.fields = {}
|
||||||
def __setitem__(B,key,value):
|
for key in dir(self.__class__):
|
||||||
A=B.__dict__.get(key)
|
obj = getattr(self.__class__, key)
|
||||||
if isinstance(A,Validator):A.value=value
|
|
||||||
def __getattr__(B,key):
|
if isinstance(obj, Validator):
|
||||||
A=B.__dict__.get(key)
|
self.__dict__[key] = copy.deepcopy(obj)
|
||||||
if isinstance(A,Validator):return A.value
|
self.__dict__[key].value = kwargs.pop(
|
||||||
return A
|
key, self.__dict__[key].initial_value
|
||||||
def set_user_data(C,data):
|
)
|
||||||
for(D,A)in data.items():
|
self.fields[key] = self.__dict__[key]
|
||||||
B=C.fields.get(D)
|
self.fields[key].model = self
|
||||||
if not B:continue
|
self.fields[key].app = kwargs.get("app")
|
||||||
if A.get(_E):A=A.get(_C)
|
|
||||||
B.value=A
|
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
|
@property
|
||||||
async def is_valid(self):return all([await A.is_valid for A in self.fields.values()])
|
async def is_valid(self):
|
||||||
def __getitem__(B,key):
|
return all([await field.is_valid for field in self.fields.values()])
|
||||||
A=B.__dict__.get(key)
|
|
||||||
if isinstance(A,Validator):return A.value
|
def __getitem__(self, key):
|
||||||
def __setattr__(A,key,value):
|
obj = self.__dict__.get(key)
|
||||||
B=value;C=getattr(A,key)
|
if isinstance(obj, Validator):
|
||||||
if isinstance(C,Validator):C.value=B
|
return obj.value
|
||||||
else:A.__dict__[key]=B
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
obj = getattr(self, key)
|
||||||
|
if isinstance(obj, Validator):
|
||||||
|
obj.value = value
|
||||||
|
else:
|
||||||
|
self.__dict__[key] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
async def recordz(self):
|
async def recordz(self):
|
||||||
D=await self.to_json();B={}
|
obj = await self.to_json()
|
||||||
for(C,A)in D.items():
|
record = {}
|
||||||
if not isinstance(A,dict)or _C not in A:continue
|
for key, value in obj.items():
|
||||||
if getattr(self,C).save:B[C]=A.get(_C)
|
if not isinstance(value, dict) or "value" not in value:
|
||||||
return B
|
continue
|
||||||
async def to_json(A,encode=_D):
|
if getattr(self, key).save:
|
||||||
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})
|
record[key] = value.get("value")
|
||||||
for(C,D)in A.fields.items():
|
return record
|
||||||
if C=='record':continue
|
|
||||||
D=A.__dict__[C]
|
async def to_json(self, encode=False):
|
||||||
if hasattr(D,_C):B[C]=await D.to_json()
|
model_data = OrderedDict(
|
||||||
if encode:return json.dumps(B,indent=2)
|
{
|
||||||
return B
|
"uid": self.uid.value,
|
||||||
|
"created_at": self.created_at.value,
|
||||||
|
"updated_at": self.updated_at.value,
|
||||||
|
"deleted_at": self.deleted_at.value,
|
||||||
|
"is_valid": await self.is_valid,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for key, value in self.fields.items():
|
||||||
|
if key == "record":
|
||||||
|
continue
|
||||||
|
value = self.__dict__[key]
|
||||||
|
if hasattr(value, "value"):
|
||||||
|
model_data[key] = await value.to_json()
|
||||||
|
if encode:
|
||||||
|
return json.dumps(model_data, indent=2)
|
||||||
|
return model_data
|
||||||
|
|
||||||
|
|
||||||
class FormElement(ModelField):
|
class FormElement(ModelField):
|
||||||
def __init__(A,place_holder=_A,*B,**C):super().__init__(*B,**C);A.place_holder=place_holder
|
|
||||||
|
def __init__(self, place_holder=None, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.place_holder = place_holder
|
||||||
|
|
||||||
|
|
||||||
class FormElement(ModelField):
|
class FormElement(ModelField):
|
||||||
def __init__(A,place_holder=_A,*B,**C):A.place_holder=place_holder;super().__init__(*B,**C)
|
|
||||||
async def to_json(B):A=await super().to_json();A[_E]=B.name;A['place_holder']=B.place_holder;return A
|
def __init__(self, place_holder=None, *args, **kwargs):
|
||||||
|
self.place_holder = place_holder
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
async def to_json(self):
|
||||||
|
data = await super().to_json()
|
||||||
|
data["name"] = self.name
|
||||||
|
data["place_holder"] = self.place_holder
|
||||||
|
return data
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
class Object:
|
class Object:
|
||||||
def __init__(A,*C,**D):
|
|
||||||
for B in C:
|
def __init__(self, *args, **kwargs):
|
||||||
if isinstance(B,dict):A.__dict__.update(B)
|
for arg in args:
|
||||||
A.__dict__.update(D)
|
if isinstance(arg, dict):
|
||||||
def __getitem__(A,key):return A.__dict__[key]
|
self.__dict__.update(arg)
|
||||||
def __setitem__(A,key,value):A.__dict__[key]=value
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.__dict__[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self.__dict__[key] = value
|
||||||
|
|||||||
@@ -1,17 +1,46 @@
|
|||||||
import cProfile,pstats,sys
|
import cProfile
|
||||||
|
import pstats
|
||||||
|
import sys
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
profiler = None
|
profiler = None
|
||||||
import io
|
import io
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def profile_middleware(request, handler):
|
async def profile_middleware(request, handler):
|
||||||
global profiler
|
global profiler
|
||||||
if not profiler:profiler=cProfile.Profile()
|
if not profiler:
|
||||||
profiler.enable();B=await handler(request);profiler.disable();A=pstats.Stats(profiler,stream=sys.stdout);A.sort_stats('cumulative');A.print_stats();return B
|
profiler = cProfile.Profile()
|
||||||
async def profiler_handler(request):A=io.StringIO();B=pstats.Stats(profiler,stream=A);C=request.query.get('sort','tot. percall');B.sort_stats(C);B.print_stats();return web.Response(text=A.getvalue())
|
profiler.enable()
|
||||||
|
response = await handler(request)
|
||||||
|
profiler.disable()
|
||||||
|
stats = pstats.Stats(profiler, stream=sys.stdout)
|
||||||
|
stats.sort_stats("cumulative")
|
||||||
|
stats.print_stats()
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def profiler_handler(request):
|
||||||
|
output = io.StringIO()
|
||||||
|
stats = pstats.Stats(profiler, stream=output)
|
||||||
|
sort_by = request.query.get("sort", "tot. percall")
|
||||||
|
stats.sort_stats(sort_by)
|
||||||
|
stats.print_stats()
|
||||||
|
return web.Response(text=output.getvalue())
|
||||||
|
|
||||||
|
|
||||||
class Profiler:
|
class Profiler:
|
||||||
def __init__(A):
|
|
||||||
|
def __init__(self):
|
||||||
global profiler
|
global profiler
|
||||||
if profiler is None:profiler=cProfile.Profile()
|
if profiler is None:
|
||||||
A.profiler=profiler
|
profiler = cProfile.Profile()
|
||||||
async def __aenter__(A):A.profiler.enable()
|
self.profiler = profiler
|
||||||
async def __aexit__(A,*B,**C):A.profiler.disable()
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
self.profiler.enable()
|
||||||
|
|
||||||
|
async def __aexit__(self, *args, **kwargs):
|
||||||
|
self.profiler.disable()
|
||||||
|
|||||||
+80
-22
@@ -1,24 +1,82 @@
|
|||||||
_A='snekker-de-snek-'
|
import hashlib
|
||||||
import hashlib,uuid
|
import uuid
|
||||||
DEFAULT_SALT=_A
|
|
||||||
DEFAULT_NS=_A
|
DEFAULT_SALT = "snekker-de-snek-"
|
||||||
|
DEFAULT_NS = "snekker-de-snek-"
|
||||||
|
|
||||||
|
|
||||||
class UIDNS:
|
class UIDNS:
|
||||||
def __init__(A,name):'Initialize UIDNS with a name.';A.name=name
|
def __init__(self, name: str) -> None:
|
||||||
|
"""Initialize UIDNS with a name."""
|
||||||
|
self.name = name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bytes(self):'Return the bytes representation of the name.';return self.name.encode()
|
def bytes(self) -> bytes:
|
||||||
def uid(value=None,ns=DEFAULT_NS):
|
"""Return the bytes representation of the name."""
|
||||||
'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
|
return self.name.encode()
|
||||||
try:ns=ns.decode()
|
|
||||||
except AttributeError:pass
|
|
||||||
if not A:A=str(uuid.uuid4())
|
def uid(value: str = None, ns: str = DEFAULT_NS) -> str:
|
||||||
try:A=A.decode()
|
"""Generate a UUID based on the provided value and namespace.
|
||||||
except AttributeError:pass
|
|
||||||
return str(uuid.uuid5(UIDNS(ns),A))
|
Args:
|
||||||
async def hash(data,salt=DEFAULT_SALT):
|
value (str): The value to generate the UUID from. If None, a new UUID is created.
|
||||||
'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
|
ns (str): The namespace to use for UUID generation.
|
||||||
try:B=B.encode(errors=C)
|
|
||||||
except AttributeError:pass
|
Returns:
|
||||||
try:A=A.encode(errors=C)
|
str: The generated UUID as a string.
|
||||||
except AttributeError:pass
|
"""
|
||||||
D=A+B;E=hashlib.sha256(D);return E.hexdigest()
|
try:
|
||||||
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
|
ns = ns.decode()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
if not value:
|
||||||
|
value = str(uuid.uuid4())
|
||||||
|
try:
|
||||||
|
value = value.decode()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return str(uuid.uuid5(UIDNS(ns), value))
|
||||||
|
|
||||||
|
|
||||||
|
def hash_sync(data: str, salt: str = DEFAULT_SALT) -> str:
|
||||||
|
"""Hash the given data with the specified salt using SHA-256.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (str): The data to hash.
|
||||||
|
salt (str): The salt to use for hashing.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The hexadecimal representation of the hashed data.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = data.encode(errors="ignore")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
salt = salt.encode(errors="ignore")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
salted = salt + data
|
||||||
|
|
||||||
|
obj = hashlib.sha256(salted)
|
||||||
|
return obj.hexdigest()
|
||||||
|
|
||||||
|
async def hash(data: str, salt: str = DEFAULT_SALT) -> str:
|
||||||
|
return hash_sync(data, salt)
|
||||||
|
|
||||||
|
def verify_sync(string: str, hashed: str) -> bool:
|
||||||
|
"""Verify if the given string matches the hashed value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
string (str): The string to verify.
|
||||||
|
hashed (str): The hashed value to compare against.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the string matches the hashed value, False otherwise.
|
||||||
|
"""
|
||||||
|
return hash_sync(string) == hashed
|
||||||
|
|
||||||
|
async def verify(string: str, hashed: str) -> bool:
|
||||||
|
return verify_sync(string, hashed)
|
||||||
|
|||||||
+62
-37
@@ -1,42 +1,67 @@
|
|||||||
_B='uid'
|
|
||||||
_A=None
|
|
||||||
from snek.mapper import get_mapper
|
from snek.mapper import get_mapper
|
||||||
from snek.model.user import UserModel
|
from snek.model.user import UserModel
|
||||||
from snek.system.mapper import BaseMapper
|
from snek.system.mapper import BaseMapper
|
||||||
|
|
||||||
|
|
||||||
class BaseService:
|
class BaseService:
|
||||||
mapper_name:BaseMapper=_A
|
|
||||||
|
mapper_name: BaseMapper = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def services(self):return self.app.services
|
def services(self):
|
||||||
def __init__(A,app):
|
return self.app.services
|
||||||
A.app=app;A.cache=app.cache
|
|
||||||
if A.mapper_name:A.mapper=get_mapper(A.mapper_name,app=A.app)
|
def __init__(self, app):
|
||||||
else:A.mapper=_A
|
self.app = app
|
||||||
async def exists(C,uid=_A,**A):
|
self.cache = app.cache
|
||||||
B=uid
|
if self.mapper_name:
|
||||||
if B:
|
self.mapper = get_mapper(self.mapper_name, app=self.app)
|
||||||
if not A and await C.cache.get(B):return True
|
else:
|
||||||
A[_B]=B
|
self.mapper = None
|
||||||
return await C.count(**A)>0
|
|
||||||
async def count(A,**B):return await A.mapper.count(**B)
|
async def exists(self, uid=None, **kwargs):
|
||||||
async def new(A,**B):return await A.mapper.new()
|
if uid:
|
||||||
async def query(A,sql,*B):
|
if not kwargs and await self.cache.get(uid):
|
||||||
for C in A.app.db.query(sql,*B):yield C
|
return True
|
||||||
async def get(B,uid=_A,**C):
|
kwargs["uid"] = uid
|
||||||
D=uid
|
return await self.count(**kwargs) > 0
|
||||||
if D:
|
|
||||||
if not C:
|
async def count(self, **kwargs):
|
||||||
A=await B.cache.get(D)
|
return await self.mapper.count(**kwargs)
|
||||||
if False and A and A.__class__==B.mapper.model_class:return A
|
|
||||||
C[_B]=D
|
async def new(self, **kwargs):
|
||||||
A=await B.mapper.get(**C)
|
return await self.mapper.new()
|
||||||
if A:await B.cache.set(A[_B],A)
|
|
||||||
return A
|
async def query(self, sql, *args):
|
||||||
async def save(B,model):
|
for record in self.app.db.query(sql, *args):
|
||||||
A=model
|
yield record
|
||||||
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 get(self, uid=None, **kwargs):
|
||||||
async def find(C,**A):
|
if uid:
|
||||||
B='_limit'
|
if not kwargs:
|
||||||
if B not in A or int(A.get(B))>30:A[B]=60
|
result = await self.cache.get(uid)
|
||||||
async for D in C.mapper.find(**A):yield D
|
if False and result and result.__class__ == self.mapper.model_class:
|
||||||
async def delete(A,**B):return await A.mapper.delete(**B)
|
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)
|
||||||
|
|||||||
+441
-52
File diff suppressed because one or more lines are too long
+108
-44
@@ -1,49 +1,113 @@
|
|||||||
_A=None
|
import asyncio
|
||||||
import asyncio,os
|
import os
|
||||||
try:import pty
|
|
||||||
except Exception as ex:print('You are not able to run a terminal. See error:');print(ex)
|
try:
|
||||||
|
import pty
|
||||||
|
except Exception as ex:
|
||||||
|
print("You are not able to run a terminal. See error:")
|
||||||
|
print(ex)
|
||||||
import subprocess
|
import subprocess
|
||||||
commands={'alpine':'docker run -it alpine /bin/sh','r':'docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh'}
|
|
||||||
|
commands = {
|
||||||
|
"alpine": "docker run -it alpine /bin/sh",
|
||||||
|
"r": "docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TerminalSession:
|
class TerminalSession:
|
||||||
def __init__(A,command):A.master,A.slave=_A,_A;A.process=_A;A.sockets=[];A.history=b'';A.history_size=20480;A.command=command;A.start_process(A.command)
|
def __init__(self, command):
|
||||||
def start_process(A,command):
|
self.master, self.slave = None, None
|
||||||
if not A.is_running():
|
self.process = None
|
||||||
if A.master:os.close(A.master);os.close(A.slave);A.master=_A;A.slave=_A
|
self.sockets = []
|
||||||
A.master,A.slave=pty.openpty();A.process=subprocess.Popen(command.split(' '),stdin=A.slave,stdout=A.slave,stderr=A.slave,bufsize=0,universal_newlines=True)
|
self.history = b""
|
||||||
def is_running(A):
|
self.history_size = 1024 * 20
|
||||||
if not A.process:return False
|
self.command = command
|
||||||
asyncio.get_event_loop();return A.process.poll()is _A
|
self.start_process(self.command)
|
||||||
async def add_websocket(A,ws):A.start_process(A.command);asyncio.create_task(A.read_output(ws))
|
|
||||||
async def read_output(A,ws):
|
def start_process(self, command):
|
||||||
B=ws;A.sockets.append(B)
|
if not self.is_running():
|
||||||
if len(A.sockets)>1 and A.history:
|
if self.master:
|
||||||
D=0
|
os.close(self.master)
|
||||||
try:D=A.history.index(b'\n')
|
os.close(self.slave)
|
||||||
except ValueError:pass
|
self.master = None
|
||||||
await B.send_bytes(A.history[D:]);return
|
self.slave = None
|
||||||
E=asyncio.get_event_loop()
|
|
||||||
|
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:
|
while True:
|
||||||
try:
|
try:
|
||||||
C=await E.run_in_executor(_A,os.read,A.master,1024)
|
data = await loop.run_in_executor(None, os.read, self.master, 1024)
|
||||||
if not C:break
|
if not data:
|
||||||
A.history+=C
|
break
|
||||||
if len(A.history)>A.history_size:A.history=A.history[:0-A.history_size]
|
self.history += data
|
||||||
|
if len(self.history) > self.history_size:
|
||||||
|
self.history = self.history[: 0 - self.history_size]
|
||||||
try:
|
try:
|
||||||
for B in A.sockets:await B.send_bytes(C)
|
for ws in self.sockets:
|
||||||
except:A.sockets.remove(B)
|
await ws.send_bytes(data) # Send raw bytes for ANSI support
|
||||||
except Exception:await A.close();break
|
except:
|
||||||
async def close(A):
|
self.sockets.remove(ws)
|
||||||
print('Terminating process')
|
except Exception:
|
||||||
if A.process:A.process.terminate();A.process=_A
|
await self.close()
|
||||||
if A.master:os.close(A.master);os.close(A.slave);A.master=_A;A.slave=_A
|
break
|
||||||
print('Terminated process')
|
|
||||||
for B in A.sockets:
|
async def close(self):
|
||||||
try:await B.close()
|
print("Terminating process")
|
||||||
except Exception:pass
|
if self.process:
|
||||||
A.sockets=[]
|
self.process.terminate()
|
||||||
async def write_input(B,data):
|
self.process = None
|
||||||
A=data
|
if self.master:
|
||||||
try:A=A.encode()
|
os.close(self.master)
|
||||||
except AttributeError:pass
|
os.close(self.slave)
|
||||||
try:await asyncio.get_event_loop().run_in_executor(_A,os.write,B.master,A)
|
self.master = None
|
||||||
except Exception as C:print(C);await B.close()
|
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()
|
||||||
|
|||||||
+63
-19
@@ -1,31 +1,75 @@
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from snek.system.markdown import render_markdown
|
from snek.system.markdown import render_markdown
|
||||||
|
|
||||||
|
|
||||||
class BaseView(web.View):
|
class BaseView(web.View):
|
||||||
|
|
||||||
login_required = False
|
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('/')
|
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()
|
return await super()._iter()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_url(self):return str(self.request.url.with_path('').with_query(''))
|
def base_url(self):
|
||||||
|
return str(self.request.url.with_path("").with_query(""))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def app(self):return self.request.app
|
def app(self):
|
||||||
|
return self.request.app
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def db(self):return self.app.db
|
def db(self):
|
||||||
|
return self.app.db
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def services(self):return self.app.services
|
def services(self):
|
||||||
async def json_response(B,data,**A):return web.json_response(data,**A)
|
return self.app.services
|
||||||
|
|
||||||
|
async def json_response(self, data, **kwargs):
|
||||||
|
return web.json_response(data, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def session(self):return self.request.session
|
def session(self):
|
||||||
async def render_template(A,template_name,context=None):
|
return self.request.session
|
||||||
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')
|
async def render_template(self, template_name, context=None):
|
||||||
return await A.request.app.render_template(B,A.request,C)
|
if template_name.endswith(".md"):
|
||||||
|
response = await self.request.app.render_template(
|
||||||
|
template_name, self.request, context
|
||||||
|
)
|
||||||
|
body = await render_markdown(self.app, response.body.decode())
|
||||||
|
return web.Response(body=body, content_type="text/html")
|
||||||
|
return await self.request.app.render_template(
|
||||||
|
template_name, self.request, context
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseFormView(BaseView):
|
class BaseFormView(BaseView):
|
||||||
|
|
||||||
form = None
|
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):
|
async def get(self):
|
||||||
E='action';C=A.form(app=A.app);D=await A.request.json();C.set_user_data(D['form']);B=await C.to_json()
|
form = self.form(app=self.app)
|
||||||
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 self.json_response(await form.to_json())
|
||||||
return await A.json_response(B)
|
|
||||||
async def submit(A,model=None):0
|
async def post(self):
|
||||||
|
form = self.form(app=self.app)
|
||||||
|
post = await self.request.json()
|
||||||
|
form.set_user_data(post["form"])
|
||||||
|
result = await form.to_json()
|
||||||
|
if post.get("action") == "validate":
|
||||||
|
# Pass
|
||||||
|
pass
|
||||||
|
if post.get("action") == "submit" and result["is_valid"]:
|
||||||
|
result = await self.submit(form)
|
||||||
|
return await self.json_response(result)
|
||||||
|
return await self.json_response(result)
|
||||||
|
|
||||||
|
async def submit(self, model=None):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
class WebSocketClient:
|
||||||
|
def __init__(self, hostname, port):
|
||||||
|
self.buffer = b''
|
||||||
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.hostname = hostname
|
||||||
|
self.port = port
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
def __getattr__(self, method, *args, **kwargs):
|
||||||
|
if method in self.__dict__.keys():
|
||||||
|
return self.__dict__[method]
|
||||||
|
def call(*args, **kwargs):
|
||||||
|
self.write(json.dumps({'method': method, 'args': args, 'kwargs': kwargs}))
|
||||||
|
return json.loads(self.read())
|
||||||
|
return call
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self.socket.connect((self.hostname, self.port))
|
||||||
|
key = base64.b64encode(b'1234123412341234').decode('utf-8')
|
||||||
|
handshake = (
|
||||||
|
f"GET /db HTTP/1.1\r\n"
|
||||||
|
f"Host: localhost:3131\r\n"
|
||||||
|
f"Upgrade: websocket\r\n"
|
||||||
|
f"Connection: Upgrade\r\n"
|
||||||
|
f"Sec-WebSocket-Key: {key}\r\n"
|
||||||
|
f"Sec-WebSocket-Version: 13\r\n\r\n"
|
||||||
|
)
|
||||||
|
self.socket.sendall(handshake.encode('utf-8'))
|
||||||
|
response = self.read_until(b'\r\n\r\n')
|
||||||
|
if b'101 Switching Protocols' not in response:
|
||||||
|
raise Exception("Failed to connect to WebSocket")
|
||||||
|
|
||||||
|
def write(self, message):
|
||||||
|
message_bytes = message.encode('utf-8')
|
||||||
|
length = len(message_bytes)
|
||||||
|
if length <= 125:
|
||||||
|
self.socket.sendall(b'\x81' + bytes([length]) + message_bytes)
|
||||||
|
elif length >= 126 and length <= 65535:
|
||||||
|
self.socket.sendall(b'\x81' + bytes([126]) + length.to_bytes(2, 'big') + message_bytes)
|
||||||
|
else:
|
||||||
|
self.socket.sendall(b'\x81' + bytes([127]) + length.to_bytes(8, 'big') + message_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def read_until(self, delimiter):
|
||||||
|
while True:
|
||||||
|
find_pos = self.buffer.find(delimiter)
|
||||||
|
if find_pos != -1:
|
||||||
|
data = self.buffer[:find_pos+4]
|
||||||
|
self.buffer = self.buffer[find_pos+4:]
|
||||||
|
return data
|
||||||
|
|
||||||
|
chunk = self.socket.recv(1024)
|
||||||
|
if not chunk:
|
||||||
|
return None
|
||||||
|
self.buffer += chunk
|
||||||
|
|
||||||
|
def read_exactly(self, length):
|
||||||
|
while len(self.buffer) < length:
|
||||||
|
chunk = self.socket.recv(length - len(self.buffer))
|
||||||
|
if not chunk:
|
||||||
|
return None
|
||||||
|
self.buffer += chunk
|
||||||
|
response = self.buffer[: length]
|
||||||
|
self.buffer = self.buffer[length:]
|
||||||
|
return response
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
frame = None
|
||||||
|
frame = self.read_exactly(2)
|
||||||
|
length = frame[1] & 127
|
||||||
|
if length == 126:
|
||||||
|
length = int.from_bytes(self.read_exactly(2), 'big')
|
||||||
|
elif length == 127:
|
||||||
|
length = int.from_bytes(self.read_exactly(8), 'big')
|
||||||
|
message = self.read_exactly(length)
|
||||||
|
return message
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.socket.close()
|
||||||
|
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<title>Snek</title>
|
<title>Snek</title>
|
||||||
<style>{{highlight_styles}}</style>
|
<style>{{highlight_styles}}</style>
|
||||||
@@ -16,6 +16,12 @@
|
|||||||
<script src="/html-frame.js" type="module"></script>
|
<script src="/html-frame.js" type="module"></script>
|
||||||
<script src="/app.js" type="module"></script>
|
<script src="/app.js" type="module"></script>
|
||||||
<script src="/file-manager.js" type="module"></script>
|
<script src="/file-manager.js" type="module"></script>
|
||||||
|
<script src="/user-list.js"></script>
|
||||||
|
<script src="/message-list.js" type="module"></script>
|
||||||
|
<script src="/chat-input.js" type="module"></script>
|
||||||
|
<link rel="stylesheet" href="/sandbox.css">
|
||||||
|
<link rel="stylesheet" href="/user-list.css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/base.css">
|
<link rel="stylesheet" href="/base.css">
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
@@ -35,6 +41,7 @@
|
|||||||
<div class="logo no-select">{% block header_text %}{% endblock %}</div>
|
<div class="logo no-select">{% block header_text %}{% endblock %}</div>
|
||||||
<nav class="no-select" style="overflow:hidden;scroll-behavior:smooth">
|
<nav class="no-select" style="overflow:hidden;scroll-behavior:smooth">
|
||||||
<a class="no-select" href="/web.html">π </a>
|
<a class="no-select" href="/web.html">π </a>
|
||||||
|
<a class="no-select" href="/drive.html">π</a>
|
||||||
<a class="no-select" href="/search-user.html">π</a>
|
<a class="no-select" href="/search-user.html">π</a>
|
||||||
<a class="no-select" style="display:none" id="install-button" href="#">π₯</a>
|
<a class="no-select" style="display:none" id="install-button" href="#">π₯</a>
|
||||||
<a class="no-select" href="/threads.html">π₯</a>
|
<a class="no-select" href="/threads.html">π₯</a>
|
||||||
@@ -43,10 +50,11 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
<main>
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
{% include "sidebar_channels.html" %}
|
{% include "sidebar_channels.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
<main>
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<chat-window class="chat-area"></chat-window>
|
<chat-window class="chat-area"></chat-window>
|
||||||
@@ -72,5 +80,6 @@ let installPrompt = null
|
|||||||
|
|
||||||
;
|
;
|
||||||
</script>
|
</script>
|
||||||
|
{% include "sandbox.html" %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user