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:
|
||||
.venv/bin/snek serve
|
||||
#$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload
|
||||
|
||||
install: ubuntu
|
||||
python3.12 -m venv .venv
|
||||
|
||||
+6
-2
@@ -18,7 +18,8 @@ dependencies = [
|
||||
"lxml",
|
||||
"IPython",
|
||||
"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",
|
||||
"gunicorn",
|
||||
"imgkit",
|
||||
@@ -33,7 +34,10 @@ dependencies = [
|
||||
"PyJWT",
|
||||
"multiavatar",
|
||||
"gitpython",
|
||||
"uvloop"
|
||||
'uvloop; platform_system != "Windows"',
|
||||
"humanize",
|
||||
"Pillow",
|
||||
"pillow-heif",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
+50
-14
@@ -1,21 +1,57 @@
|
||||
_D='Database path for the application'
|
||||
_C='snek.db'
|
||||
_B='--db_path'
|
||||
_A=True
|
||||
import click,uvloop
|
||||
import click
|
||||
import uvloop
|
||||
from aiohttp import web
|
||||
import asyncio
|
||||
from snek.app import Application
|
||||
from IPython import start_ipython
|
||||
import sqlite3
|
||||
import pathlib
|
||||
import shutil
|
||||
|
||||
@click.group()
|
||||
def cli():0
|
||||
def cli():
|
||||
pass
|
||||
|
||||
@cli.command()
|
||||
@click.option('--port',default=8081,show_default=_A,help='Port to run the application on')
|
||||
@click.option('--host',default='0.0.0.0',show_default=_A,help='Host to run the application on')
|
||||
@click.option(_B,default=_C,show_default=_A,help=_D)
|
||||
def serve(port,host,db_path):asyncio.set_event_loop_policy(uvloop.EventLoopPolicy());web.run_app(Application(db_path=f"sqlite:///{db_path}"),port=port,host=host)
|
||||
@click.option('--db_path',default="snek.db", help='Database to initialize if not exists.')
|
||||
@click.option('--source',default=None, help='Database to initialize if not exists.')
|
||||
def init(db_path,source):
|
||||
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()
|
||||
@click.option(_B,default=_C,show_default=_A,help=_D)
|
||||
def shell(db_path):A=Application(db_path=f"sqlite:///{db_path}");start_ipython(argv=[],user_ns={'app':A})
|
||||
def main():cli()
|
||||
if __name__=='__main__':main()
|
||||
@click.option('--port', default=8081, show_default=True, help='Port to run the application on')
|
||||
@click.option('--host', default='0.0.0.0', show_default=True, help='Host to run the application on')
|
||||
@click.option('--db_path', default='snek.db', show_default=True, help='Database path for the application')
|
||||
def serve(port, host, db_path):
|
||||
#init(db_path)
|
||||
#asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
web.run_app(
|
||||
Application(db_path=f"sqlite:///{db_path}"), port=port, host=host
|
||||
)
|
||||
|
||||
@cli.command()
|
||||
@click.option('--db_path', default='snek.db', show_default=True, help='Database path for the application')
|
||||
def shell(db_path):
|
||||
app = Application(db_path=f"sqlite:///{db_path}")
|
||||
start_ipython(argv=[], user_ns={'app': app})
|
||||
|
||||
def main():
|
||||
cli()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+342
-81
@@ -1,32 +1,41 @@
|
||||
_G='name'
|
||||
_F='static'
|
||||
_E='user'
|
||||
_D=None
|
||||
_C=True
|
||||
_B='channel_uid'
|
||||
_A='uid'
|
||||
import asyncio,logging,pathlib,time,uuid
|
||||
import asyncio
|
||||
import logging
|
||||
import pathlib
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from snek import snode
|
||||
from snek.view.threads import ThreadsView
|
||||
import json
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp_session import get_session as session_get,session_middleware,setup as session_setup
|
||||
from aiohttp_session import (
|
||||
get_session as session_get,
|
||||
session_middleware,
|
||||
setup as session_setup,
|
||||
)
|
||||
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
||||
from app.app import Application as BaseApplication
|
||||
from jinja2 import FileSystemLoader
|
||||
|
||||
from snek.sssh import start_ssh_server
|
||||
from snek.docs.app import Application as DocsApplication
|
||||
from snek.mapper import get_mappers
|
||||
from snek.service import get_services
|
||||
from snek.system import http
|
||||
from snek.system.cache import Cache
|
||||
from snek.system.markdown import MarkdownExtension
|
||||
from snek.system.middleware import auth_middleware,cors_middleware
|
||||
from snek.system.middleware import auth_middleware, cors_middleware
|
||||
from snek.system.profiler import profiler_handler
|
||||
from snek.system.template import EmojiExtension,LinkifyExtension,PythonExtension
|
||||
from snek.view.about import AboutHTMLView,AboutMDView
|
||||
from snek.system.template import EmojiExtension, LinkifyExtension, PythonExtension
|
||||
from snek.view.about import AboutHTMLView, AboutMDView
|
||||
from snek.view.avatar import AvatarView
|
||||
from snek.view.docs import DocsHTMLView,DocsMDView
|
||||
from snek.view.docs import DocsHTMLView, DocsMDView
|
||||
from snek.view.drive import DriveView
|
||||
from snek.view.drive import DriveApiView
|
||||
from snek.view.index import IndexView
|
||||
from snek.view.login import LoginView
|
||||
from snek.view.logout import LogoutView
|
||||
@@ -42,79 +51,331 @@ from snek.view.settings.index import SettingsIndexView
|
||||
from snek.view.settings.profile import SettingsProfileView
|
||||
from snek.view.stats import StatsView
|
||||
from snek.view.status import StatusView
|
||||
from snek.view.terminal import TerminalSocketView,TerminalView
|
||||
from snek.view.terminal import TerminalSocketView, TerminalView
|
||||
from snek.view.upload import UploadView
|
||||
from snek.view.user import UserView
|
||||
from snek.view.web import WebView
|
||||
from snek.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.sgit import GitApplication
|
||||
SESSION_KEY=b'c79a0c5fda4b424189c427d28c9f7c34'
|
||||
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def session_middleware(request,handler):A=request;setattr(A,'session',await session_get(A));B=await handler(A);return B
|
||||
async def session_middleware(request, handler):
|
||||
setattr(request, "session", await session_get(request))
|
||||
response = await handler(request)
|
||||
return response
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def trailing_slash_middleware(request,handler):
|
||||
A=request
|
||||
if A.path and not A.path.endswith('/'):raise web.HTTPFound(A.path+'/')
|
||||
return await handler(A)
|
||||
async def trailing_slash_middleware(request, handler):
|
||||
if request.path and not request.path.endswith("/"):
|
||||
# Redirect to the same path with a trailing slash
|
||||
raise web.HTTPFound(request.path + "/")
|
||||
return await handler(request)
|
||||
|
||||
|
||||
class Application(BaseApplication):
|
||||
def __init__(A,*B,**C):D=[cors_middleware,web.normalize_path_middleware(merge_slashes=_C)];A.template_path=pathlib.Path(__file__).parent.joinpath('templates');A.static_path=pathlib.Path(__file__).parent.joinpath(_F);super().__init__(middlewares=D,template_path=A.template_path,client_max_size=5368709120*B,**C);session_setup(A,EncryptedCookieStorage(SESSION_KEY));A.tasks=asyncio.Queue();A._middlewares.append(session_middleware);A._middlewares.append(auth_middleware);A.jinja2_env.add_extension(MarkdownExtension);A.jinja2_env.add_extension(LinkifyExtension);A.jinja2_env.add_extension(PythonExtension);A.jinja2_env.add_extension(EmojiExtension);A.setup_router();A.executor=_D;A.cache=Cache(A);A.services=get_services(app=A);A.mappers=get_mappers(app=A);A.on_startup.append(A.prepare_asyncio);A.on_startup.append(A.prepare_database)
|
||||
async def prepare_asyncio(A,app):app.executor=ThreadPoolExecutor(max_workers=200);app.loop.set_default_executor(A.executor)
|
||||
async def create_task(A,task):await A.tasks.put(task)
|
||||
async def task_runner(A):
|
||||
while _C:
|
||||
B=await A.tasks.get();A.db.begin()
|
||||
try:C=time.time();await B;D=time.time();print(f"Task {B} took {D-C} seconds");A.tasks.task_done()
|
||||
except Exception as E:print(E)
|
||||
A.db.commit()
|
||||
async def prepare_database(A,app):
|
||||
C='channel_message';D='channel_member';E='username';B='user_uid';A.db.query('PRAGMA journal_mode=WAL');A.db.query('PRAGMA syncnorm=off')
|
||||
try:
|
||||
if not A.db[_E].has_index(E):A.db[_E].create_index(E,unique=_C)
|
||||
if not A.db[D].has_index([_B,B]):A.db[D].create_index([_B,B])
|
||||
if not A.db[C].has_index([_B,B]):A.db[C].create_index([_B,B])
|
||||
except:pass
|
||||
await app.services.drive.prepare_all();A.loop.create_task(A.task_runner())
|
||||
def setup_router(A):A.router.add_get('/',IndexView);A.router.add_static('/',pathlib.Path(__file__).parent.joinpath(_F),name=_F,show_index=_C);A.router.add_view('/profiler.html',profiler_handler);A.router.add_view('/about.html',AboutHTMLView);A.router.add_view('/about.md',AboutMDView);A.router.add_view('/logout.json',LogoutView);A.router.add_view('/logout.html',LogoutView);A.router.add_view('/docs.html',DocsHTMLView);A.router.add_view('/docs.md',DocsMDView);A.router.add_view('/status.json',StatusView);A.router.add_view('/settings/index.html',SettingsIndexView);A.router.add_view('/settings/profile.html',SettingsProfileView);A.router.add_view('/settings/profile.json',SettingsProfileView);A.router.add_view('/web.html',WebView);A.router.add_view('/login.html',LoginView);A.router.add_view('/login.json',LoginView);A.router.add_view('/register.html',RegisterView);A.router.add_view('/register.json',RegisterView);A.router.add_view('/drive/{rel_path:.*}',DriveView);A.router.add_view('/drive.bin',UploadView);A.router.add_view('/drive.bin/{uid}.{ext}',UploadView);A.router.add_view('/search-user.html',SearchUserView);A.router.add_view('/search-user.json',SearchUserView);A.router.add_view('/avatar/{uid}.svg',AvatarView);A.router.add_get('/http-get',A.handle_http_get);A.router.add_get('/http-photo',A.handle_http_photo);A.router.add_get('/rpc.ws',RPCView);A.router.add_view('/channel/{channel}.html',WebView);A.router.add_view('/threads.html',ThreadsView);A.router.add_view('/terminal.ws',TerminalSocketView);A.router.add_view('/terminal.html',TerminalView);A.router.add_view('/drive.json',DriveView);A.router.add_view('/drive/{drive}.json',DriveView);A.router.add_view('/stats.json',StatsView);A.router.add_view('/user/{user}.html',UserView);A.router.add_view('/repository/{username}/{repo_name}',RepositoryView);A.router.add_view('/repository/{username}/{repo_name}/{rel_path:.*}',RepositoryView);A.router.add_view('/settings/repositories/index.html',RepositoriesIndexView);A.router.add_view('/settings/repositories/create.html',RepositoriesCreateView);A.router.add_view('/settings/repositories/repository/{name}/update.html',RepositoriesUpdateView);A.router.add_view('/settings/repositories/repository/{name}/delete.html',RepositoriesDeleteView);A.webdav=WebdavApplication(A);A.git=GitApplication(A);A.add_subapp('/webdav',A.webdav);A.add_subapp('/git',A.git)
|
||||
async def handle_test(A,request):return await A.render_template('test.html',request,context={_G:'retoor'})
|
||||
async def handle_http_get(C,request):A=request.query.get('url');B=await http.get(A);return web.Response(body=B)
|
||||
async def handle_http_photo(C,request):A=request.query.get('url');B=await http.create_site_photo(A);return web.Response(body=B.read_bytes(),headers={'Content-Type':'image/png'})
|
||||
async def render_template(A,template,request,context=_D):
|
||||
I='channels';J='new_count';K='color';L=template;F='last_message_on';D=request;C=context;G=[]
|
||||
if not C:C={}
|
||||
C['rid']=str(uuid.uuid4())
|
||||
if D.session.get(_A):
|
||||
async for E in A.services.channel_member.find(user_uid=D.session.get(_A),deleted_at=_D,is_banned=False):
|
||||
B={};M=await A.services.channel_member.get_other_dm_user(E[_B],D.session.get(_A));H=await E.get_channel();N=await H.get_last_message();O=_D
|
||||
if N:P=await N.get_user();O=P[K]
|
||||
B[K]=O;B[F]=H[F];B['is_private']=H['tag']=='dm'
|
||||
if M:B[_G]=M['nick'];B[_A]=E[_B]
|
||||
else:B[_G]=E['label'];B[_A]=E[_B]
|
||||
B[J]=E[J];G.append(B)
|
||||
G.sort(key=lambda x:x[F]or'',reverse=_C)
|
||||
if I not in C:C[I]=G
|
||||
if _E not in C:C[_E]=await A.services.user.get(D.session.get(_A))
|
||||
A.template_path.joinpath(L);await A.services.user.get_template_path(D.session.get(_A));A.original_loader=A.jinja2_env.loader;A.jinja2_env.loader=await A.get_user_template_loader(D.session.get(_A));Q=await super().render_template(L,D,C);A.jinja2_env.loader=A.original_loader;return Q
|
||||
async def static_handler(B,request):
|
||||
D=request;E=D.match_info.get('filename','');C=[];F=D.session.get(_A)
|
||||
if F:
|
||||
A=await B.services.user.get_static_path(F)
|
||||
if A:C.append(A)
|
||||
for H in B.services.user.get_admin_uids():
|
||||
A=await B.services.user.get_static_path(H)
|
||||
if A:C.append(A)
|
||||
C.append(B.static_path)
|
||||
for G in C:
|
||||
if pathlib.Path(G).joinpath(E).exists():return web.FileResponse(pathlib.Path(G).joinpath(E))
|
||||
return web.HTTPNotFound()
|
||||
async def get_user_template_loader(B,uid=_D):
|
||||
C=[]
|
||||
for D in B.services.user.get_admin_uids():
|
||||
A=await B.services.user.get_template_path(D)
|
||||
if A:C.append(A)
|
||||
if uid:
|
||||
A=await B.services.user.get_template_path(uid)
|
||||
if A:C.append(A)
|
||||
C.append(B.template_path);return FileSystemLoader(C)
|
||||
app=Application(db_path='sqlite:///snek.db')
|
||||
async def main():await web._run_app(app,port=8081,host='0.0.0.0')
|
||||
if __name__=='__main__':asyncio.run(main())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
middlewares = [
|
||||
cors_middleware,
|
||||
web.normalize_path_middleware(merge_slashes=True),
|
||||
]
|
||||
self.template_path = pathlib.Path(__file__).parent.joinpath("templates")
|
||||
self.static_path = pathlib.Path(__file__).parent.joinpath("static")
|
||||
super().__init__(
|
||||
middlewares=middlewares, template_path=self.template_path, client_max_size=1024*1024*1024*5 *args, **kwargs
|
||||
)
|
||||
session_setup(self, EncryptedCookieStorage(SESSION_KEY))
|
||||
self.tasks = asyncio.Queue()
|
||||
self._middlewares.append(session_middleware)
|
||||
self._middlewares.append(auth_middleware)
|
||||
self.jinja2_env.add_extension(MarkdownExtension)
|
||||
self.jinja2_env.add_extension(LinkifyExtension)
|
||||
self.jinja2_env.add_extension(PythonExtension)
|
||||
self.jinja2_env.add_extension(EmojiExtension)
|
||||
self.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:
|
||||
await task
|
||||
self.tasks.task_done()
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
self.db.commit()
|
||||
|
||||
async def prepare_database(self, app):
|
||||
self.db.query("PRAGMA journal_mode=WAL")
|
||||
self.db.query("PRAGMA syncnorm=off")
|
||||
|
||||
try:
|
||||
if not self.db["user"].has_index("username"):
|
||||
self.db["user"].create_index("username", unique=True)
|
||||
if not self.db["channel_member"].has_index(["channel_uid", "user_uid"]):
|
||||
self.db["channel_member"].create_index(["channel_uid", "user_uid"])
|
||||
if not self.db["channel_message"].has_index(["channel_uid", "user_uid"]):
|
||||
self.db["channel_message"].create_index(["channel_uid", "user_uid"])
|
||||
except:
|
||||
pass
|
||||
|
||||
await app.services.drive.prepare_all()
|
||||
self.loop.create_task(self.task_runner())
|
||||
|
||||
def setup_router(self):
|
||||
self.router.add_get("/", IndexView)
|
||||
self.router.add_static(
|
||||
"/",
|
||||
pathlib.Path(__file__).parent.joinpath("static"),
|
||||
name="static",
|
||||
show_index=True,
|
||||
)
|
||||
self.router.add_view("/profiler.html", profiler_handler)
|
||||
self.router.add_view("/about.html", AboutHTMLView)
|
||||
self.router.add_view("/about.md", AboutMDView)
|
||||
self.router.add_view("/logout.json", LogoutView)
|
||||
self.router.add_view("/logout.html", LogoutView)
|
||||
self.router.add_view("/docs.html", DocsHTMLView)
|
||||
self.router.add_view("/docs.md", DocsMDView)
|
||||
self.router.add_view("/status.json", StatusView)
|
||||
self.router.add_view("/settings/index.html", SettingsIndexView)
|
||||
self.router.add_view("/settings/profile.html", SettingsProfileView)
|
||||
self.router.add_view("/settings/profile.json", SettingsProfileView)
|
||||
self.router.add_view("/web.html", WebView)
|
||||
self.router.add_view("/login.html", LoginView)
|
||||
self.router.add_view("/login.json", LoginView)
|
||||
self.router.add_view("/register.html", RegisterView)
|
||||
self.router.add_view("/register.json", RegisterView)
|
||||
self.router.add_view("/drive/{rel_path:.*}", DriveView)
|
||||
self.router.add_view("/drive.bin", UploadView)
|
||||
self.router.add_view("/drive.bin/{uid}.{ext}", UploadView)
|
||||
self.router.add_view("/search-user.html", SearchUserView)
|
||||
self.router.add_view("/search-user.json", SearchUserView)
|
||||
self.router.add_view("/avatar/{uid}.svg", AvatarView)
|
||||
self.router.add_get("/http-get", self.handle_http_get)
|
||||
self.router.add_get("/http-photo", self.handle_http_photo)
|
||||
self.router.add_get("/rpc.ws", RPCView)
|
||||
self.router.add_get("/c/{channel:.*}", ChannelView)
|
||||
self.router.add_view("/channel/{channel_uid}/attachment.bin",ChannelAttachmentView)
|
||||
self.router.add_view("/channel/attachment/{relative_url:.*}",ChannelAttachmentView)
|
||||
self.router.add_view("/channel/{channel}.html", WebView)
|
||||
self.router.add_view("/threads.html", ThreadsView)
|
||||
self.router.add_view("/terminal.ws", TerminalSocketView)
|
||||
self.router.add_view("/terminal.html", TerminalView)
|
||||
self.router.add_view("/drive.json", DriveApiView)
|
||||
self.router.add_view("/drive.html", DriveView)
|
||||
self.router.add_view("/drive/{drive}.json", DriveView)
|
||||
self.router.add_view("/stats.json", StatsView)
|
||||
self.router.add_view("/user/{user}.html", UserView)
|
||||
self.router.add_view("/repository/{username}/{repository}", RepositoryView)
|
||||
self.router.add_view("/repository/{username}/{repository}/{path:.*}", RepositoryView)
|
||||
self.router.add_view("/settings/repositories/index.html", RepositoriesIndexView)
|
||||
self.router.add_view("/settings/repositories/create.html", RepositoriesCreateView)
|
||||
self.router.add_view("/settings/repositories/repository/{name}/update.html", RepositoriesUpdateView)
|
||||
self.router.add_view("/settings/repositories/repository/{name}/delete.html", RepositoriesDeleteView)
|
||||
self.router.add_view("/settings/containers/index.html", ContainersIndexView)
|
||||
self.router.add_view("/settings/containers/create.html", ContainersCreateView)
|
||||
self.router.add_view("/settings/containers/container/{uid}/update.html", ContainersUpdateView)
|
||||
self.router.add_view("/settings/containers/container/{uid}/delete.html", ContainersDeleteView)
|
||||
self.webdav = WebdavApplication(self)
|
||||
self.git = GitApplication(self)
|
||||
self.add_subapp("/webdav", self.webdav)
|
||||
self.add_subapp("/git",self.git)
|
||||
|
||||
#self.router.add_get("/{file_path:.*}", self.static_handler)
|
||||
|
||||
async def handle_test(self, request):
|
||||
|
||||
return await self.render_template(
|
||||
"test.html", request, context={"name": "retoor"}
|
||||
)
|
||||
|
||||
async def handle_http_get(self, request: web.Request):
|
||||
url = request.query.get("url")
|
||||
content = await http.get(url)
|
||||
return web.Response(body=content)
|
||||
|
||||
async def handle_http_photo(self, request):
|
||||
url = request.query.get("url")
|
||||
path = await http.create_site_photo(url)
|
||||
return web.Response(
|
||||
body=path.read_bytes(), headers={"Content-Type": "image/png"}
|
||||
)
|
||||
|
||||
# @time_cache_async(60)
|
||||
async def render_template(self, template, request, context=None):
|
||||
channels = []
|
||||
if not context:
|
||||
context = {}
|
||||
context["rid"] = str(uuid.uuid4())
|
||||
if request.session.get("uid"):
|
||||
async for subscribed_channel in self.services.channel_member.find(
|
||||
user_uid=request.session.get("uid"), deleted_at=None, is_banned=False
|
||||
):
|
||||
|
||||
parent_object = await subscribed_channel.get_channel()
|
||||
|
||||
item = {}
|
||||
other_user = await self.services.channel_member.get_other_dm_user(
|
||||
subscribed_channel["channel_uid"], request.session.get("uid")
|
||||
)
|
||||
|
||||
last_message = await parent_object.get_last_message()
|
||||
color = None
|
||||
if last_message:
|
||||
last_message_user = await last_message.get_user()
|
||||
color = last_message_user["color"]
|
||||
item["color"] = color
|
||||
item["last_message_on"] = parent_object["last_message_on"]
|
||||
item["is_private"] = parent_object["tag"] == "dm"
|
||||
if other_user:
|
||||
item["name"] = other_user["nick"]
|
||||
item["uid"] = subscribed_channel["channel_uid"]
|
||||
else:
|
||||
item["name"] = subscribed_channel["label"]
|
||||
item["uid"] = subscribed_channel["channel_uid"]
|
||||
item["new_count"] = subscribed_channel["new_count"]
|
||||
|
||||
channels.append(item)
|
||||
|
||||
channels.sort(key=lambda x: x["last_message_on"] or "", reverse=True)
|
||||
if "channels" not in context:
|
||||
context["channels"] = channels
|
||||
if "user" not in context:
|
||||
context["user"] = await self.services.user.get(
|
||||
request.session.get("uid")
|
||||
)
|
||||
|
||||
self.template_path.joinpath(template)
|
||||
|
||||
await self.services.user.get_template_path(request.session.get("uid"))
|
||||
|
||||
self.original_loader = self.jinja2_env.loader
|
||||
|
||||
self.jinja2_env.loader = await self.get_user_template_loader(
|
||||
request.session.get("uid")
|
||||
)
|
||||
|
||||
rendered = await super().render_template(template, request, context)
|
||||
|
||||
self.jinja2_env.loader = self.original_loader
|
||||
|
||||
return rendered
|
||||
|
||||
|
||||
async def static_handler(self, request):
|
||||
file_name = request.match_info.get('filename', '')
|
||||
|
||||
paths = []
|
||||
|
||||
uid = request.session.get("uid")
|
||||
if uid:
|
||||
user_static_path = await self.services.user.get_static_path(uid)
|
||||
if user_static_path:
|
||||
paths.append(user_static_path)
|
||||
|
||||
for admin_uid in self.services.user.get_admin_uids():
|
||||
user_static_path = await self.services.user.get_static_path(admin_uid)
|
||||
if user_static_path:
|
||||
paths.append(user_static_path)
|
||||
|
||||
paths.append(self.static_path)
|
||||
|
||||
for path in paths:
|
||||
if pathlib.Path(path).joinpath(file_name).exists():
|
||||
return web.FileResponse(pathlib.Path(path).joinpath(file_name))
|
||||
return web.HTTPNotFound()
|
||||
|
||||
async def get_user_template_loader(self, uid=None):
|
||||
template_paths = []
|
||||
for admin_uid in self.services.user.get_admin_uids():
|
||||
user_template_path = await self.services.user.get_template_path(admin_uid)
|
||||
if user_template_path:
|
||||
template_paths.append(user_template_path)
|
||||
|
||||
if uid:
|
||||
user_template_path = await self.services.user.get_template_path(uid)
|
||||
if user_template_path:
|
||||
template_paths.append(user_template_path)
|
||||
|
||||
|
||||
template_paths.append(self.template_path)
|
||||
return FileSystemLoader(template_paths)
|
||||
|
||||
|
||||
app = Application(db_path="sqlite:///snek.db")
|
||||
|
||||
|
||||
async def main():
|
||||
await web._run_app(app, port=8081, host="0.0.0.0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -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
|
||||
|
||||
from aiohttp import web
|
||||
from app.app import Application as BaseApplication
|
||||
|
||||
from snek.system.markdown import MarkdownExtension
|
||||
|
||||
|
||||
class Application(BaseApplication):
|
||||
def __init__(A,path=None,*B,**C):A.path=pathlib.Path(path);D=A.path;super().__init__(*B,template_path=D,**C);A.jinja2_env.add_extension(MarkdownExtension);A.router.add_get('/{tail:.*}',A.handle_document)
|
||||
async def handle_document(B,request):
|
||||
D='text/plain';E=b'Resource is not found on this server.';F='index.html';G=request;C=G.match_info['tail'].strip('/')
|
||||
if C=='':C=F
|
||||
A=B.path.joinpath(C)
|
||||
if not A.exists():return web.Response(status=404,body=E,content_type=D)
|
||||
if A.is_dir():A=A.joinpath(F)
|
||||
if not A.exists():return web.Response(status=404,body=E,content_type=D)
|
||||
H=await B.render_template(str(A.relative_to(B.path)),G);return H
|
||||
|
||||
def __init__(self, path=None, *args, **kwargs):
|
||||
self.path = pathlib.Path(path)
|
||||
template_path = self.path
|
||||
|
||||
super().__init__(template_path=template_path, *args, **kwargs)
|
||||
self.jinja2_env.add_extension(MarkdownExtension)
|
||||
|
||||
self.router.add_get("/{tail:.*}", self.handle_document)
|
||||
|
||||
async def handle_document(self, request):
|
||||
relative_path = request.match_info["tail"].strip("/")
|
||||
if relative_path == "":
|
||||
relative_path = "index.html"
|
||||
document_path = self.path.joinpath(relative_path)
|
||||
if not document_path.exists():
|
||||
return web.Response(
|
||||
status=404,
|
||||
body=b"Resource is not found on this server.",
|
||||
content_type="text/plain",
|
||||
)
|
||||
if document_path.is_dir():
|
||||
document_path = document_path.joinpath("index.html")
|
||||
if not document_path.exists():
|
||||
return web.Response(
|
||||
status=404,
|
||||
body=b"Resource is not found on this server.",
|
||||
content_type="text/plain",
|
||||
)
|
||||
|
||||
response = await self.render_template(
|
||||
str(document_path.relative_to(self.path)), request
|
||||
)
|
||||
return response
|
||||
|
||||
+36
-9
@@ -1,12 +1,39 @@
|
||||
_B='created_at'
|
||||
_A='uid'
|
||||
import asyncio
|
||||
|
||||
from snek.app import app
|
||||
async def fix_message(message):C='user';D='text';B='user_uid';A=message;A={_A:A[_A],B:A[B],D:A['message'],'sent':A[_B]};E=await app.services.user.get(uid=A[B]);A[C]=E and E['username']or None;return(A[C]or'')+': '+(A[D]or'')
|
||||
|
||||
|
||||
async def fix_message(message):
|
||||
message = {
|
||||
"uid": message["uid"],
|
||||
"user_uid": message["user_uid"],
|
||||
"text": message["message"],
|
||||
"sent": message["created_at"],
|
||||
}
|
||||
user = await app.services.user.get(uid=message["user_uid"])
|
||||
message["user"] = user and user["username"] or None
|
||||
return (message["user"] or "") + ": " + (message["text"] or "")
|
||||
|
||||
|
||||
async def dump_public_channels():
|
||||
A=[]
|
||||
for B in app.db['channel'].find(is_private=False,is_listed=True,tag='public'):print(f"Dumping channel: {B["label"]}.");A+=[await fix_message(A)for A in app.db['channel_message'].find(channel_uid=B[_A],order_by=_B)];print('Dump succesfull!')
|
||||
print('Converting to json.');print('Converting succesful, now writing to dump.json')
|
||||
with open('dump.txt','w')as C:C.write('\n\n'.join(A))
|
||||
print('Dump written to dump.json')
|
||||
if __name__=='__main__':asyncio.run(dump_public_channels())
|
||||
result = []
|
||||
for channel in app.db["channel"].find(
|
||||
is_private=False, is_listed=True, tag="public"
|
||||
):
|
||||
print(f"Dumping channel: {channel['label']}.")
|
||||
result += [
|
||||
await fix_message(record)
|
||||
for record in app.db["channel_message"].find(
|
||||
channel_uid=channel["uid"], order_by="created_at"
|
||||
)
|
||||
]
|
||||
print("Dump succesfull!")
|
||||
print("Converting to json.")
|
||||
print("Converting succesful, now writing to dump.json")
|
||||
with open("dump.txt", "w") as f:
|
||||
f.write("\n\n".join(result))
|
||||
print("Dump written to dump.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(dump_public_channels())
|
||||
|
||||
+49
-12
@@ -1,14 +1,51 @@
|
||||
_B='username'
|
||||
_A='password'
|
||||
from snek.system.form import Form,FormButtonElement,FormInputElement,HTMLElement
|
||||
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
|
||||
|
||||
|
||||
class AuthField(FormInputElement):
|
||||
@property
|
||||
async def errors(self):
|
||||
A=self;B=await super().errors
|
||||
if A.model.password.value and A.model.username.value:
|
||||
if not await A.app.services.user.validate_login(A.model.username.value,A.model.password.value):return['Invalid username or password']
|
||||
return B
|
||||
|
||||
@property
|
||||
async def errors(self):
|
||||
result = await super().errors
|
||||
if self.model.password.value and self.model.username.value:
|
||||
if not await self.app.services.user.validate_login(
|
||||
self.model.username.value, self.model.password.value
|
||||
):
|
||||
return ["Invalid username or password"]
|
||||
return result
|
||||
|
||||
|
||||
class LoginForm(Form):
|
||||
title=HTMLElement(tag='h1',text='Login');username=AuthField(name=_B,required=True,min_length=2,max_length=20,regex='^[a-zA-Z0-9_-]+$',place_holder='Username',type='text');password=AuthField(name=_A,required=True,min_length=1,type=_A,place_holder='Password');action=FormButtonElement(name='action',value='submit',text='Login',type='button')
|
||||
@property
|
||||
async def is_valid(self):A=self;return all([A[_B],A[_A],not await A.username.errors,not await A.password.errors])
|
||||
|
||||
title = HTMLElement(tag="h1", text="Login")
|
||||
|
||||
username = AuthField(
|
||||
name="username",
|
||||
required=True,
|
||||
min_length=2,
|
||||
max_length=20,
|
||||
regex=r"^[a-zA-Z0-9_-]+$",
|
||||
place_holder="Username",
|
||||
type="text",
|
||||
)
|
||||
password = AuthField(
|
||||
name="password",
|
||||
required=True,
|
||||
min_length=1,
|
||||
type="password",
|
||||
place_holder="Password",
|
||||
)
|
||||
|
||||
action = FormButtonElement(
|
||||
name="action", value="submit", text="Login", type="button"
|
||||
)
|
||||
|
||||
@property
|
||||
async def is_valid(self):
|
||||
return all(
|
||||
[
|
||||
self["username"],
|
||||
self["password"],
|
||||
not await self.username.errors,
|
||||
not await self.password.errors,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,10 +1,44 @@
|
||||
_B='password'
|
||||
_A='Register'
|
||||
from snek.system.form import Form,FormButtonElement,FormInputElement,HTMLElement
|
||||
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
|
||||
|
||||
|
||||
class UsernameField(FormInputElement):
|
||||
@property
|
||||
async def errors(self):
|
||||
A=self;B=await super().errors
|
||||
if A.value and await A.app.services.user.count(username=A.value):B.append('Username is not available.')
|
||||
return B
|
||||
class RegisterForm(Form):title=HTMLElement(tag='h1',text=_A);username=UsernameField(name='username',required=True,min_length=2,max_length=20,regex='^[a-zA-Z0-9_-]+$',place_holder='Username',type='text');email=FormInputElement(name='email',required=False,regex='^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$',place_holder='Email address',type='email');password=FormInputElement(name=_B,required=True,min_length=1,type=_B,place_holder='Password');action=FormButtonElement(name='action',value='submit',text=_A,type='button')
|
||||
|
||||
@property
|
||||
async def errors(self):
|
||||
result = await super().errors
|
||||
if self.value and await self.app.services.user.count(username=self.value):
|
||||
result.append("Username is not available.")
|
||||
return result
|
||||
|
||||
|
||||
class RegisterForm(Form):
|
||||
|
||||
title = HTMLElement(tag="h1", text="Register")
|
||||
|
||||
username = UsernameField(
|
||||
name="username",
|
||||
required=True,
|
||||
min_length=2,
|
||||
max_length=20,
|
||||
regex=r"^[a-zA-Z0-9_-]+$",
|
||||
place_holder="Username",
|
||||
type="text",
|
||||
)
|
||||
email = FormInputElement(
|
||||
name="email",
|
||||
required=False,
|
||||
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
|
||||
place_holder="Email address",
|
||||
type="email",
|
||||
)
|
||||
password = FormInputElement(
|
||||
name="password",
|
||||
required=True,
|
||||
min_length=1,
|
||||
type="password",
|
||||
place_holder="Password",
|
||||
)
|
||||
|
||||
action = FormButtonElement(
|
||||
name="action", value="submit", text="Register", type="button"
|
||||
)
|
||||
|
||||
@@ -1,2 +1,18 @@
|
||||
from snek.system.form import Form,FormButtonElement,FormInputElement,HTMLElement
|
||||
class SearchUserForm(Form):title=HTMLElement(tag='h1',text='Search user');username=FormInputElement(name='username',required=True,min_length=1,max_length=128,place_holder='Username');action=FormButtonElement(name='action',value='submit',text='Search',type='button')
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
_C='button'
|
||||
_B='submit'
|
||||
_A='action'
|
||||
from snek.system.form import Form,FormButtonElement,FormInputElement,HTMLElement
|
||||
class SettingsProfileForm(Form):nick=FormInputElement(name='nick',required=True,place_holder='Your Nickname',min_length=1,max_length=20);action=FormButtonElement(name=_A,value=_B,text='Save',type=_C);title=HTMLElement(tag='h1',text='Profile');profile=FormInputElement(name='profile',place_holder='Tell about yourself.',required=False,max_length=300);action=FormButtonElement(name=_A,value=_B,text='Save',type=_C)
|
||||
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
|
||||
|
||||
|
||||
class SettingsProfileForm(Form):
|
||||
|
||||
nick = FormInputElement(
|
||||
name="nick",
|
||||
required=True,
|
||||
place_holder="Your Nickname",
|
||||
min_length=1,
|
||||
max_length=20,
|
||||
)
|
||||
action = FormButtonElement(
|
||||
name="action", value="submit", text="Save", type="button"
|
||||
)
|
||||
title = HTMLElement(tag="h1", text="Profile")
|
||||
profile = FormInputElement(
|
||||
name="profile",
|
||||
place_holder="Tell about yourself.",
|
||||
required=False,
|
||||
max_length=300,
|
||||
)
|
||||
action = FormButtonElement(
|
||||
name="action", value="submit", text="Save", type="button"
|
||||
)
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from snek.app import app
|
||||
application=app
|
||||
|
||||
application = app
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import functools
|
||||
|
||||
from snek.mapper.channel import ChannelMapper
|
||||
from snek.mapper.channel_member import ChannelMemberMapper
|
||||
from snek.mapper.channel_message import ChannelMessageMapper
|
||||
@@ -8,7 +9,30 @@ from snek.mapper.notification import NotificationMapper
|
||||
from snek.mapper.user import UserMapper
|
||||
from snek.mapper.user_property import UserPropertyMapper
|
||||
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
|
||||
|
||||
|
||||
@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.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.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.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.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.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.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.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.system.mapper import BaseMapper
|
||||
|
||||
|
||||
class UserMapper(BaseMapper):
|
||||
table_name='user';model_class=UserModel
|
||||
def get_admin_uids(A):
|
||||
try:return[A['uid']for A in A.db.query('SELECT uid FROM user WHERE is_admin = :is_admin',{'is_admin':True})]
|
||||
except Exception as B:print(B);return[]
|
||||
table_name = "user"
|
||||
model_class = UserModel
|
||||
|
||||
def get_admin_uids(self):
|
||||
try:
|
||||
return [
|
||||
user["uid"]
|
||||
for user in self.db.query(
|
||||
"SELECT uid FROM user WHERE is_admin = :is_admin",
|
||||
{"is_admin": True},
|
||||
)
|
||||
]
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
return []
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from snek.model.user_property import UserPropertyModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
class UserPropertyMapper(BaseMapper):table_name='user_property';model_class=UserPropertyModel
|
||||
|
||||
|
||||
class UserPropertyMapper(BaseMapper):
|
||||
table_name = "user_property"
|
||||
model_class = UserPropertyModel
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import functools
|
||||
|
||||
from snek.model.channel import ChannelModel
|
||||
from snek.model.channel_member import ChannelMemberModel
|
||||
|
||||
# from snek.model.channel_message import ChannelMessageModel
|
||||
from snek.model.channel_message import ChannelMessageModel
|
||||
from snek.model.drive import DriveModel
|
||||
from snek.model.drive_item import DriveItemModel
|
||||
@@ -8,7 +11,29 @@ from snek.model.notification import NotificationModel
|
||||
from snek.model.user import UserModel
|
||||
from snek.model.user_property import UserPropertyModel
|
||||
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
|
||||
|
||||
|
||||
@functools.cache
|
||||
def get_models():return Object(**{'user':UserModel,'channel_member':ChannelMemberModel,'channel':ChannelModel,'channel_message':ChannelMessageModel,'drive_item':DriveItemModel,'drive':DriveModel,'notification':NotificationModel,'user_property':UserPropertyModel,'repository':RepositoryModel})
|
||||
def get_model(name):return get_models()[name]
|
||||
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,
|
||||
"channel_attachment": ChannelAttachmentModel,
|
||||
"container": Container,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_model(name):
|
||||
return get_models()[name]
|
||||
|
||||
+28
-10
@@ -1,12 +1,30 @@
|
||||
_C='uid'
|
||||
_B=False
|
||||
_A=True
|
||||
from snek.model.channel_message import ChannelMessageModel
|
||||
from snek.system.model import BaseModel,ModelField
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
class ChannelModel(BaseModel):
|
||||
label=ModelField(name='label',required=_A,kind=str);description=ModelField(name='description',required=_B,kind=str);tag=ModelField(name='tag',required=_B,kind=str);created_by_uid=ModelField(name='created_by_uid',required=_A,kind=str);is_private=ModelField(name='is_private',required=_A,kind=bool,value=_B);is_listed=ModelField(name='is_listed',required=_A,kind=bool,value=_A);index=ModelField(name='index',required=_A,kind=int,value=1000);last_message_on=ModelField(name='last_message_on',required=_B,kind=str)
|
||||
async def get_last_message(A):
|
||||
try:
|
||||
async for B in A.app.services.channel_message.query('SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1',{'channel_uid':A[_C]}):return await A.app.services.channel_message.get(uid=B[_C])
|
||||
except:pass
|
||||
async def get_members(A):return await A.app.services.channel_member.find(channel_uid=A[_C],deleted_at=None,is_banned=_B)
|
||||
label = ModelField(name="label", required=True, kind=str)
|
||||
description = ModelField(name="description", required=False, kind=str)
|
||||
tag = ModelField(name="tag", required=False, kind=str)
|
||||
created_by_uid = ModelField(name="created_by_uid", required=True, kind=str)
|
||||
is_private = ModelField(name="is_private", required=True, kind=bool, value=False)
|
||||
is_listed = ModelField(name="is_listed", required=True, kind=bool, value=True)
|
||||
index = ModelField(name="index", required=True, kind=int, value=1000)
|
||||
last_message_on = ModelField(name="last_message_on", required=False, kind=str)
|
||||
|
||||
async def get_last_message(self) -> ChannelMessageModel:
|
||||
try:
|
||||
async for model in self.app.services.channel_message.query(
|
||||
"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1",
|
||||
{"channel_uid": self["uid"]},
|
||||
):
|
||||
|
||||
return await self.app.services.channel_message.get(uid=model["uid"])
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_members(self):
|
||||
return await self.app.services.channel_member.find(
|
||||
channel_uid=self["uid"], deleted_at=None, is_banned=False
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
label=ModelField(name='label',required=_A,kind=str);channel_uid=ModelField(name=_D,required=_A,kind=str);user_uid=ModelField(name=_C,required=_A,kind=str);is_moderator=ModelField(name='is_moderator',required=_A,kind=bool,value=_B);is_read_only=ModelField(name='is_read_only',required=_A,kind=bool,value=_B);is_muted=ModelField(name='is_muted',required=_A,kind=bool,value=_B);is_banned=ModelField(name='is_banned',required=_A,kind=bool,value=_B);new_count=ModelField(name='new_count',required=_B,kind=int,value=0)
|
||||
async def get_user(A):return await A.app.services.user.get(uid=A[_C])
|
||||
async def get_channel(A):return await A.app.services.channel.get(uid=A[_D])
|
||||
async def get_name(A):
|
||||
B=await A.get_channel()
|
||||
if B['tag']=='dm':C=await A.get_other_dm_user();return C['nick']
|
||||
return B['name']or A['label']
|
||||
async def get_other_dm_user(A):
|
||||
B='uid';C=await A.get_channel()
|
||||
if C['tag']!='dm':return
|
||||
async for D in A.app.services.channel_member.find(channel_uid=C[B]):
|
||||
if D[B]!=A[B]:return await A.app.services.user.get(uid=D[_C])
|
||||
return await A.get_user()
|
||||
label = ModelField(name="label", required=True, kind=str)
|
||||
channel_uid = ModelField(name="channel_uid", required=True, kind=str)
|
||||
user_uid = ModelField(name="user_uid", required=True, kind=str)
|
||||
is_moderator = ModelField(
|
||||
name="is_moderator", required=True, kind=bool, value=False
|
||||
)
|
||||
is_read_only = ModelField(
|
||||
name="is_read_only", required=True, kind=bool, value=False
|
||||
)
|
||||
is_muted = ModelField(name="is_muted", required=True, kind=bool, value=False)
|
||||
is_banned = ModelField(name="is_banned", required=True, kind=bool, value=False)
|
||||
new_count = ModelField(name="new_count", required=False, kind=int, value=0)
|
||||
|
||||
async def get_user(self):
|
||||
return await self.app.services.user.get(uid=self["user_uid"])
|
||||
|
||||
async def get_channel(self):
|
||||
return await self.app.services.channel.get(uid=self["channel_uid"])
|
||||
|
||||
async def get_name(self):
|
||||
channel = await self.get_channel()
|
||||
if channel["tag"] == "dm":
|
||||
user = await self.get_other_dm_user()
|
||||
return user["nick"]
|
||||
return channel["name"] or self["label"]
|
||||
|
||||
async def get_other_dm_user(self):
|
||||
channel = await self.get_channel()
|
||||
if channel["tag"] != "dm":
|
||||
return None
|
||||
|
||||
async for model in self.app.services.channel_member.find(
|
||||
channel_uid=channel["uid"]
|
||||
):
|
||||
if model["uid"] != self["uid"]:
|
||||
return await self.app.services.user.get(uid=model["user_uid"])
|
||||
return await self.get_user()
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
_B='user_uid'
|
||||
_A='channel_uid'
|
||||
from snek.model.user import UserModel
|
||||
from snek.system.model import BaseModel,ModelField
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
from datetime import datetime,timezone
|
||||
|
||||
class ChannelMessageModel(BaseModel):
|
||||
channel_uid=ModelField(name=_A,required=True,kind=str);user_uid=ModelField(name=_B,required=True,kind=str);message=ModelField(name='message',required=True,kind=str);html=ModelField(name='html',required=False,kind=str)
|
||||
async def get_user(A):return await A.app.services.user.get(uid=A[_B])
|
||||
async def get_channel(A):return await A.app.services.channel.get(uid=A[_A])
|
||||
channel_uid = ModelField(name="channel_uid", required=True, kind=str)
|
||||
user_uid = ModelField(name="user_uid", required=True, kind=str)
|
||||
message = ModelField(name="message", required=True, kind=str)
|
||||
html = ModelField(name="html", required=False, kind=str)
|
||||
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)
|
||||
+13
-5
@@ -1,6 +1,14 @@
|
||||
from snek.system.model import BaseModel,ModelField
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
class DriveModel(BaseModel):
|
||||
user_uid=ModelField(name='user_uid',required=True);name=ModelField(name='name',required=False,type=str)
|
||||
@property
|
||||
async def items(self):
|
||||
async for A in self.app.services.drive_item.find(drive_uid=self['uid']):yield A
|
||||
|
||||
user_uid = ModelField(name="user_uid", required=True)
|
||||
name = ModelField(name="name", required=False, type=str)
|
||||
|
||||
@property
|
||||
async def items(self):
|
||||
async for drive_item in self.app.services.drive_item.find(
|
||||
drive_uid=self["uid"]
|
||||
):
|
||||
yield drive_item
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
_B='name'
|
||||
_A=True
|
||||
import mimetypes
|
||||
from snek.system.model import BaseModel,ModelField
|
||||
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
class DriveItemModel(BaseModel):
|
||||
drive_uid=ModelField(name='drive_uid',required=_A,kind=str);name=ModelField(name=_B,required=_A,kind=str);path=ModelField(name='path',required=_A,kind=str);file_type=ModelField(name='file_type',required=_A,kind=str);file_size=ModelField(name='file_size',required=_A,kind=int);is_available=ModelField(name='is_available',required=_A,kind=bool,initial_value=_A)
|
||||
@property
|
||||
def extension(self):return self[_B].split('.')[-1]
|
||||
@property
|
||||
def mime_type(self):A,B=mimetypes.guess_type(self[_B]);return A
|
||||
drive_uid = ModelField(name="drive_uid", required=True, kind=str)
|
||||
name = ModelField(name="name", required=True, kind=str)
|
||||
path = ModelField(name="path", required=True, kind=str)
|
||||
file_type = ModelField(name="file_type", required=True, kind=str)
|
||||
file_size = ModelField(name="file_size", required=True, kind=int)
|
||||
is_available = ModelField(name="is_available", required=True, kind=bool, initial_value=True)
|
||||
|
||||
@property
|
||||
def extension(self):
|
||||
return self["name"].split(".")[-1]
|
||||
|
||||
@property
|
||||
def mime_type(self):
|
||||
mimetype, _ = mimetypes.guess_type(self["name"])
|
||||
return mimetype
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
_A=True
|
||||
from snek.system.model import BaseModel,ModelField
|
||||
class NotificationModel(BaseModel):object_uid=ModelField(name='object_uid',required=_A);object_type=ModelField(name='object_type',required=_A);message=ModelField(name='message',required=_A);user_uid=ModelField(name='user_uid',required=_A);read_at=ModelField(name='is_read',required=_A)
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
class NotificationModel(BaseModel):
|
||||
object_uid = ModelField(name="object_uid", required=True)
|
||||
object_type = ModelField(name="object_type", required=True)
|
||||
message = ModelField(name="message", required=True)
|
||||
user_uid = ModelField(name="user_uid", required=True)
|
||||
read_at = ModelField(name="is_read", required=True)
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
from snek.model.user import UserModel
|
||||
from snek.system.model import BaseModel,ModelField
|
||||
class RepositoryModel(BaseModel):user_uid=ModelField(name='user_uid',required=True,kind=str);name=ModelField(name='name',required=True,kind=str);is_private=ModelField(name='is_private',required=False,kind=bool)
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
+59
-16
@@ -1,17 +1,60 @@
|
||||
_D='^[a-zA-Z0-9_-+/]+$'
|
||||
_C=False
|
||||
_B=True
|
||||
_A='uid'
|
||||
from snek.system.model import BaseModel,ModelField
|
||||
from snek.system.model import BaseModel, ModelField
|
||||
|
||||
|
||||
class UserModel(BaseModel):
|
||||
username=ModelField(name='username',required=_B,min_length=2,max_length=20,regex=_D);nick=ModelField(name='nick',required=_B,min_length=2,max_length=20,regex=_D);color=ModelField(name='color',required=_B,regex='^#[0-9a-fA-F]{6}$',kind=str);email=ModelField(name='email',required=_C,regex='^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$');password=ModelField(name='password',required=_B,min_length=1);last_ping=ModelField(name='last_ping',required=_C,kind=str);is_admin=ModelField(name='is_admin',required=_C,kind=bool)
|
||||
async def get_property(A,name):
|
||||
B=await A.app.services.user_property.find_one(user_uid=A[_A],name=name)
|
||||
if B:return B['value']
|
||||
async def has_property(A,name):return await A.app.services.user_property.exists(user_uid=A[_A],name=name)
|
||||
async def set_property(A,name,value):
|
||||
C=value;B=name
|
||||
if not await A.has_property(B):await A.app.services.user_property.insert(user_uid=A[_A],name=B,value=C)
|
||||
else:await A.app.services.user_property.update(user_uid=A[_A],name=B,value=C)
|
||||
async def get_channel_members(A):
|
||||
async for B in A.app.services.channel_member.find(user_uid=A[_A],is_banned=_C,deleted_at=None):yield B
|
||||
|
||||
username = ModelField(
|
||||
name="username",
|
||||
required=True,
|
||||
min_length=2,
|
||||
max_length=20,
|
||||
regex=r"^[a-zA-Z0-9_-+/]+$",
|
||||
)
|
||||
nick = ModelField(
|
||||
name="nick",
|
||||
required=True,
|
||||
min_length=2,
|
||||
max_length=20,
|
||||
regex=r"^[a-zA-Z0-9_-+/]+$",
|
||||
)
|
||||
color = ModelField(
|
||||
name="color", required=True, regex=r"^#[0-9a-fA-F]{6}$", kind=str
|
||||
)
|
||||
email = ModelField(
|
||||
name="email",
|
||||
required=False,
|
||||
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
|
||||
)
|
||||
password = ModelField(name="password", required=True, min_length=1)
|
||||
|
||||
last_ping = ModelField(name="last_ping", required=False, kind=str)
|
||||
|
||||
is_admin = ModelField(name="is_admin", required=False, kind=bool)
|
||||
|
||||
async def get_property(self, name):
|
||||
prop = await self.app.services.user_property.find_one(
|
||||
user_uid=self["uid"], name=name
|
||||
)
|
||||
if prop:
|
||||
return prop["value"]
|
||||
|
||||
async def has_property(self, name):
|
||||
return await self.app.services.user_property.exists(
|
||||
user_uid=self["uid"], name=name
|
||||
)
|
||||
|
||||
async def set_property(self, name, value):
|
||||
if not await self.has_property(name):
|
||||
await self.app.services.user_property.insert(
|
||||
user_uid=self["uid"], name=name, value=value
|
||||
)
|
||||
else:
|
||||
await self.app.services.user_property.update(
|
||||
user_uid=self["uid"], name=name, value=value
|
||||
)
|
||||
|
||||
async def get_channel_members(self):
|
||||
async for channel_member in self.app.services.channel_member.find(
|
||||
user_uid=self["uid"], is_banned=False, deleted_at=None
|
||||
):
|
||||
yield channel_member
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
from snek.system.model import BaseModel,ModelField
|
||||
class UserPropertyModel(BaseModel):user_uid=ModelField(name='user_uid',required=True,kind=str);name=ModelField(name='name',required=True,kind=str);value=ModelField(name='path',required=True,kind=str)
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
from snek.service.channel import ChannelService
|
||||
from snek.service.channel_member import ChannelMemberService
|
||||
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.util import UtilService
|
||||
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.service.db import DBService
|
||||
|
||||
@functools.cache
|
||||
def get_services(app):A=app;return Object(**{'user':UserService(app=A),'channel_member':ChannelMemberService(app=A),'channel':ChannelService(app=A),'channel_message':ChannelMessageService(app=A),'chat':ChatService(app=A),'socket':SocketService(app=A),'notification':NotificationService(app=A),'util':UtilService(app=A),'drive':DriveService(app=A),'drive_item':DriveItemService(app=A),'user_property':UserPropertyService(app=A),'repository':RepositoryService(app=A)})
|
||||
def get_service(name,app=None):return get_services(app=app)[name]
|
||||
def get_services(app):
|
||||
return Object(
|
||||
**{
|
||||
"user": UserService(app=app),
|
||||
"channel_member": ChannelMemberService(app=app),
|
||||
"channel": ChannelService(app=app),
|
||||
"channel_message": ChannelMessageService(app=app),
|
||||
"chat": ChatService(app=app),
|
||||
"socket": SocketService(app=app),
|
||||
"notification": NotificationService(app=app),
|
||||
"util": UtilService(app=app),
|
||||
"drive": DriveService(app=app),
|
||||
"drive_item": DriveItemService(app=app),
|
||||
"user_property": UserPropertyService(app=app),
|
||||
"repository": RepositoryService(app=app),
|
||||
"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 snek.system.model import now
|
||||
from snek.system.service import BaseService
|
||||
|
||||
import pathlib
|
||||
|
||||
class ChannelService(BaseService):
|
||||
mapper_name='channel'
|
||||
async def get(E,uid=_B,**A):
|
||||
D='name';C=uid
|
||||
if C:
|
||||
A[_C]=C;B=await super().get(**A)
|
||||
if B:return B
|
||||
del A[_C];A[D]=C;B=await super().get(**A)
|
||||
if B:return B
|
||||
A[D]='#'+C;B=await super().get(**A)
|
||||
if B:return B
|
||||
return
|
||||
return await super().get(**A)
|
||||
async def create(C,label,created_by_uid,description=_B,tag=_B,is_private=_A,is_listed=_D):
|
||||
E=is_listed;D=tag;B=label
|
||||
if B[0]!='#'and E:B=f"#{B}"
|
||||
F=await C.count(deleted_at=_B)
|
||||
if not D and not F:D=_E
|
||||
A=await C.new();A['label']=B;A['description']=description;A['tag']=D;A['created_by_uid']=created_by_uid;A['is_private']=is_private;A['is_listed']=E
|
||||
if await C.save(A):return A
|
||||
raise Exception(f"Failed to create channel: {A.errors}.")
|
||||
async def get_dm(A,user1,user2):
|
||||
C=user2;B=user1;D=await A.services.channel_member.get_dm(B,C)
|
||||
if D:return await A.get(uid=D[_F])
|
||||
E=await A.create('DM',B,tag='dm');await A.services.channel_member.create_dm(E[_C],B,C);return E
|
||||
async def get_users(A,channel_uid):
|
||||
async for C in A.services.channel_member.find(channel_uid=channel_uid,is_banned=_A,is_muted=_A,deleted_at=_B):
|
||||
B=await A.services.user.get(uid=C['user_uid'])
|
||||
if B:yield B
|
||||
async def get_online_users(C,channel_uid):
|
||||
B='last_ping'
|
||||
async for A in C.get_users(channel_uid):
|
||||
if not A[B]:continue
|
||||
if(datetime.fromisoformat(now())-datetime.fromisoformat(A[B])).total_seconds()<20:yield A
|
||||
async def get_for_user(A,user_uid):
|
||||
async for B in A.services.channel_member.find(user_uid=user_uid,is_banned=_A,deleted_at=_B):C=await A.get(uid=B[_F]);yield C
|
||||
async def ensure_public_channel(B,created_by_uid):
|
||||
C=created_by_uid;A=await B.get(is_listed=_D,tag=_E);D=_A
|
||||
if not A:D=_D;A=await B.create(_E,created_by_uid=C,is_listed=_D,tag=_E)
|
||||
await B.app.services.channel_member.create(A[_C],C,is_moderator=D,is_read_only=_A,is_muted=_A,is_banned=_A);return A
|
||||
mapper_name = "channel"
|
||||
|
||||
async def get_attachment_folder(self, channel_uid,ensure=False):
|
||||
path = pathlib.Path(f"./drive/{channel_uid}/attachments")
|
||||
if ensure:
|
||||
path.mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
return path
|
||||
|
||||
async def get(self, uid=None, **kwargs):
|
||||
if uid:
|
||||
kwargs["uid"] = uid
|
||||
result = await super().get(**kwargs)
|
||||
if result:
|
||||
return result
|
||||
del kwargs["uid"]
|
||||
kwargs["name"] = uid
|
||||
result = await super().get(**kwargs)
|
||||
if result:
|
||||
return result
|
||||
kwargs["name"] = "#" + uid
|
||||
result = await super().get(**kwargs)
|
||||
if result:
|
||||
return result
|
||||
return None
|
||||
return await super().get(**kwargs)
|
||||
|
||||
async def create(
|
||||
self,
|
||||
label,
|
||||
created_by_uid,
|
||||
description=None,
|
||||
tag=None,
|
||||
is_private=False,
|
||||
is_listed=True,
|
||||
):
|
||||
if label[0] != "#" and is_listed:
|
||||
label = f"#{label}"
|
||||
count = await self.count(deleted_at=None)
|
||||
if not tag and not count:
|
||||
tag = "public"
|
||||
model = await self.new()
|
||||
model["label"] = label
|
||||
model["description"] = description
|
||||
model["tag"] = tag
|
||||
model["created_by_uid"] = created_by_uid
|
||||
model["is_private"] = is_private
|
||||
model["is_listed"] = is_listed
|
||||
if await self.save(model):
|
||||
return model
|
||||
raise Exception(f"Failed to create channel: {model.errors}.")
|
||||
|
||||
async def get_dm(self, user1, user2):
|
||||
channel_member = await self.services.channel_member.get_dm(user1, user2)
|
||||
if channel_member:
|
||||
return await self.get(uid=channel_member["channel_uid"])
|
||||
channel = await self.create("DM", user1, tag="dm")
|
||||
await self.services.channel_member.create_dm(channel["uid"], user1, user2)
|
||||
return channel
|
||||
|
||||
async def get_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
|
||||
|
||||
|
||||
class ChannelMemberService(BaseService):
|
||||
mapper_name='channel_member'
|
||||
async def mark_as_read(A,channel_uid,user_uid):B=await A.get(channel_uid=channel_uid,user_uid=user_uid);B['new_count']=0;return await A.save(B)
|
||||
async def get_user_uids(A,channel_uid):
|
||||
async for B in A.mapper.query('SELECT user_uid FROM channel_member WHERE channel_uid=:channel_uid',{_B:channel_uid}):yield B[_C]
|
||||
async def create(B,channel_uid,user_uid,is_moderator=_A,is_read_only=_A,is_muted=_A,is_banned=_A):
|
||||
D='label';E='is_banned';F=user_uid;C=channel_uid;A=await B.get(channel_uid=C,user_uid=F)
|
||||
if A:
|
||||
if A[E]:return _A
|
||||
return A
|
||||
A=await B.new();G=await B.services.channel.get(uid=C);A[D]=G[D];A[_B]=C;A[_C]=F;A['is_moderator']=is_moderator;A['is_read_only']=is_read_only;A['is_muted']=is_muted;A[E]=is_banned
|
||||
if await B.save(A):return A
|
||||
raise Exception(f"Failed to create channel member: {A.errors}.")
|
||||
async def get_dm(D,from_user,to_user):
|
||||
E='to_user';F='from_user';A=to_user;B=from_user
|
||||
async for C in D.query("SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') INNER JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = channel.uid AND channel_member2.user_uid = :to_user) WHERE channel_member.user_uid=:from_user ",{F:B,E:A}):return C
|
||||
if not B==A:return
|
||||
async for C in D.query("SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') LEFT JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = NULL AND channel_member2.user_uid = NULL) WHERE channel_member.user_uid=:from_user ",{F:B,E:A}):return C
|
||||
async def get_other_dm_user(A,channel_uid,user_uid):
|
||||
B='uid';C=channel_uid;D=await A.get(channel_uid=C,user_uid=user_uid);F=await A.services.channel.get(uid=D[_B])
|
||||
if F['tag']!='dm':return
|
||||
async for E in A.services.channel_member.find(channel_uid=C):
|
||||
if E[B]!=D[B]:return await A.services.user.get(uid=E[_C])
|
||||
async def create_dm(A,channel_uid,from_user_uid,to_user_uid):B=channel_uid;C=await A.create(B,from_user_uid);await A.create(B,to_user_uid);return C
|
||||
|
||||
mapper_name = "channel_member"
|
||||
|
||||
async def mark_as_read(self, channel_uid, user_uid):
|
||||
channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)
|
||||
channel_member["new_count"] = 0
|
||||
return await self.save(channel_member)
|
||||
|
||||
async def get_user_uids(self, channel_uid):
|
||||
async for model in self.mapper.query(
|
||||
"SELECT user_uid FROM channel_member WHERE channel_uid=:channel_uid",
|
||||
{"channel_uid": channel_uid},
|
||||
):
|
||||
yield model["user_uid"]
|
||||
|
||||
async def create(
|
||||
self,
|
||||
channel_uid,
|
||||
user_uid,
|
||||
is_moderator=False,
|
||||
is_read_only=False,
|
||||
is_muted=False,
|
||||
is_banned=False,
|
||||
):
|
||||
model = await self.get(channel_uid=channel_uid, user_uid=user_uid)
|
||||
if model:
|
||||
if model["is_banned"]:
|
||||
return False
|
||||
return model
|
||||
model = await self.new()
|
||||
channel = await self.services.channel.get(uid=channel_uid)
|
||||
model["label"] = channel["label"]
|
||||
model["channel_uid"] = channel_uid
|
||||
model["user_uid"] = user_uid
|
||||
model["is_moderator"] = is_moderator
|
||||
model["is_read_only"] = is_read_only
|
||||
model["is_muted"] = is_muted
|
||||
model["is_banned"] = is_banned
|
||||
if await self.save(model):
|
||||
return model
|
||||
raise Exception(f"Failed to create channel member: {model.errors}.")
|
||||
|
||||
async def get_dm(self, from_user, to_user):
|
||||
async for model in self.query(
|
||||
"SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') INNER JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = channel.uid AND channel_member2.user_uid = :to_user) WHERE channel_member.user_uid=:from_user ",
|
||||
{"from_user": from_user, "to_user": to_user},
|
||||
):
|
||||
return model
|
||||
if not from_user == to_user:
|
||||
return None
|
||||
async for model in self.query(
|
||||
"SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') LEFT JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = NULL AND channel_member2.user_uid = NULL) WHERE channel_member.user_uid=:from_user ",
|
||||
{"from_user": from_user, "to_user": to_user},
|
||||
):
|
||||
|
||||
return model
|
||||
|
||||
async def get_other_dm_user(self, channel_uid, user_uid):
|
||||
channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)
|
||||
channel = await self.services.channel.get(uid=channel_member["channel_uid"])
|
||||
if channel["tag"] != "dm":
|
||||
return None
|
||||
async for model in self.services.channel_member.find(channel_uid=channel_uid):
|
||||
if model["uid"] != channel_member["uid"]:
|
||||
return await self.services.user.get(uid=model["user_uid"])
|
||||
|
||||
async def create_dm(self, channel_uid, from_user_uid, to_user_uid):
|
||||
result = await self.create(channel_uid, from_user_uid)
|
||||
await self.create(channel_uid, to_user_uid)
|
||||
return result
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
class ChannelMessageService(BaseService):
|
||||
mapper_name='channel_message'
|
||||
async def create(B,channel_uid,user_uid,message):
|
||||
E=user_uid;A=await B.new();A[_A]=channel_uid;A[_B]=E;A[_E]=message;D={};F=A.record;D.update(F);C=await B.app.services.user.get(uid=E);D.update({_B:C[_F],_C:C[_C],_I:C['nick'],_D:C[_D]})
|
||||
try:G=B.app.jinja2_env.get_template('message.html');A[_G]=G.render(**D)
|
||||
except Exception as H:print(H,flush=True)
|
||||
if await B.save(A):return A
|
||||
raise Exception(f"Failed to create channel message: {A.errors}.")
|
||||
async def to_extended_dict(C,message):
|
||||
A=message;B=await C.services.user.get(uid=A[_B])
|
||||
if not B:return{}
|
||||
return{_F:A[_F],_D:B[_D],_B:A[_B],_A:A[_A],_I:B['nick'],_E:A[_E],_H:A[_H],_G:A[_G],_C:B[_C]}
|
||||
async def offset(D,channel_uid,page=0,timestamp=None,page_size=30):
|
||||
J='timestamp';E='offset';F='page_size';G=timestamp;H=channel_uid;C=page_size;A=[];I=page*C
|
||||
try:
|
||||
if G:
|
||||
async for B in D.query('SELECT * FROM channel_message WHERE channel_uid=:channel_uid AND created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size OFFSET :offset',{_A:H,F:C,E:I,J:G}):A.append(B)
|
||||
elif page>0:
|
||||
async for B in D.query('SELECT * FROM channel_message WHERE channel_uid=:channel_uid WHERE created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size',{_A:H,F:C,E:I,J:G}):A.append(B)
|
||||
else:
|
||||
async for B in D.query('SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT :page_size OFFSET :offset',{_A:H,F:C,E:I}):A.append(B)
|
||||
except:pass
|
||||
A.sort(key=lambda x:x[_H]);return A
|
||||
mapper_name = "channel_message"
|
||||
|
||||
async def create(self, channel_uid, user_uid, message, is_final=True):
|
||||
model = await self.new()
|
||||
|
||||
model["channel_uid"] = channel_uid
|
||||
model["user_uid"] = user_uid
|
||||
model["message"] = message
|
||||
model['is_final'] = is_final
|
||||
|
||||
context = {}
|
||||
|
||||
record = model.record
|
||||
context.update(record)
|
||||
user = await self.app.services.user.get(uid=user_uid)
|
||||
context.update(
|
||||
{
|
||||
"user_uid": user["uid"],
|
||||
"username": user["username"],
|
||||
"user_nick": user["nick"],
|
||||
"color": user["color"],
|
||||
}
|
||||
)
|
||||
try:
|
||||
template = self.app.jinja2_env.get_template("message.html")
|
||||
model["html"] = template.render(**context)
|
||||
except Exception as ex:
|
||||
print(ex, flush=True)
|
||||
|
||||
if await super().save(model):
|
||||
return model
|
||||
raise Exception(f"Failed to create channel message: {model.errors}.")
|
||||
|
||||
async def to_extended_dict(self, message):
|
||||
user = await self.services.user.get(uid=message["user_uid"])
|
||||
if not user:
|
||||
return {}
|
||||
return {
|
||||
"uid": message["uid"],
|
||||
"color": user["color"],
|
||||
"user_uid": message["user_uid"],
|
||||
"channel_uid": message["channel_uid"],
|
||||
"user_nick": user["nick"],
|
||||
"message": message["message"],
|
||||
"created_at": message["created_at"],
|
||||
"html": message["html"],
|
||||
"username": user["username"],
|
||||
}
|
||||
|
||||
async def save(self, model):
|
||||
context = {}
|
||||
context.update(model.record)
|
||||
user = await self.app.services.user.get(model['user_uid'])
|
||||
context.update(
|
||||
{
|
||||
"user_uid": user["uid"],
|
||||
"username": user["username"],
|
||||
"user_nick": user["nick"],
|
||||
"color": user["color"],
|
||||
}
|
||||
)
|
||||
template = self.app.jinja2_env.get_template("message.html")
|
||||
model["html"] = template.render(**context)
|
||||
return await super().save(model)
|
||||
|
||||
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
||||
results = []
|
||||
offset = page * page_size
|
||||
try:
|
||||
if timestamp:
|
||||
async for model in self.query(
|
||||
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid AND created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",
|
||||
{
|
||||
"channel_uid": channel_uid,
|
||||
"page_size": page_size,
|
||||
"offset": offset,
|
||||
"timestamp": timestamp,
|
||||
},
|
||||
):
|
||||
results.append(model)
|
||||
elif page > 0:
|
||||
async for model in self.query(
|
||||
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid WHERE created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size",
|
||||
{
|
||||
"channel_uid": channel_uid,
|
||||
"page_size": page_size,
|
||||
"offset": offset,
|
||||
"timestamp": timestamp,
|
||||
},
|
||||
):
|
||||
results.append(model)
|
||||
else:
|
||||
async for model in self.query(
|
||||
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",
|
||||
{
|
||||
"channel_uid": channel_uid,
|
||||
"page_size": page_size,
|
||||
"offset": offset,
|
||||
},
|
||||
):
|
||||
results.append(model)
|
||||
|
||||
except:
|
||||
pass
|
||||
results.sort(key=lambda x: x["created_at"])
|
||||
return results
|
||||
|
||||
@@ -1,7 +1,66 @@
|
||||
from snek.system.model import now
|
||||
from snek.system.service import BaseService
|
||||
|
||||
|
||||
class ChatService(BaseService):
|
||||
async def send(A,user_uid,channel_uid,message):
|
||||
H='username';I='created_at';J='color';K='html';L='message';D='uid';E=user_uid;C=channel_uid;F=await A.services.channel.get(uid=C)
|
||||
if not F:raise Exception('Channel not found.')
|
||||
B=await A.services.channel_message.create(C,E,message);M=B[D];G=await A.services.user.get(uid=E);F['last_message_on']=now();await A.services.channel.save(F);await A.services.socket.broadcast(C,{L:B[L],K:B[K],'user_uid':E,J:G[J],'channel_uid':C,I:B[I],'updated_at':None,H:G[H],D:B[D],'user_nick':G['nick']});await A.app.create_task(A.services.notification.create_channel_message(M));return True
|
||||
|
||||
async def finalize(self, message_uid):
|
||||
channel_message = await self.services.channel_message.get(uid=message_uid)
|
||||
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
|
||||
|
||||
|
||||
class DriveService(BaseService):
|
||||
mapper_name='drive';EXTENSIONS_PICTURES=['jpg','jpeg','png','gif','svg','webp','tiff'];EXTENSIONS_VIDEOS=['mp4','m4v','mov','wmv','webm','mkv','mpg','mpeg','avi','ogv','ogg','flv','3gp','3g2'];EXTENSIONS_ARCHIVES=['zip','rar','7z','tar','tar.gz','tar.xz','tar.bz2','tar.lzma','tar.lz'];EXTENSIONS_AUDIO=['mp3','wav','ogg','flac','m4a','wma','aac','opus','aiff','au','mid','midi'];EXTENSIONS_DOCS=['pdf','doc','docx','xls','xlsx','ppt','pptx','txt','md','json','csv','xml','html','css','js','py','sql','rs','toml','yml','yaml','ini','conf','config','log','csv','tsv','java','cs','csproj','scss','less','sass','json','lock','lock.json','jsonl']
|
||||
async def get_drive_name_by_extension(B,extension):
|
||||
A=extension
|
||||
if A.startswith('.'):A=A[1:]
|
||||
if A in B.EXTENSIONS_PICTURES:return _E
|
||||
if A in B.EXTENSIONS_VIDEOS:return _F
|
||||
if A in B.EXTENSIONS_ARCHIVES:return _G
|
||||
if A in B.EXTENSIONS_AUDIO:return'Audio'
|
||||
if A in B.EXTENSIONS_DOCS:return _H
|
||||
return _B
|
||||
async def get_drive_by_extension(A,user_uid,extension):B=await A.get_drive_name_by_extension(extension);return await A.get_or_create(user_uid=user_uid,name=B)
|
||||
async def get_by_user(C,user_uid,name=None):
|
||||
B=name;D={_C:user_uid}
|
||||
async for A in C.find(**D):
|
||||
if not B:yield A
|
||||
elif A[_A]==B:yield A
|
||||
elif not A[_A]and B==_B:A[_A]=_B;await C.save(A);yield A
|
||||
async def get_or_create(B,user_uid,name=None,extensions=None):
|
||||
D=user_uid;C=name;E={_C:D}
|
||||
if C:E[_A]=C
|
||||
async for A in B.get_by_user(**E):return A
|
||||
A=await B.new();A[_C]=D;A[_A]=C;await B.save(A);return A
|
||||
async def prepare_default_drives(B):
|
||||
C='drive_uid'
|
||||
async for A in B.services.drive_item.find():
|
||||
E=A.extension;D=await B.get_drive_by_extension(A[_C],E)
|
||||
if not A[C]==D[_D]:A[C]=D[_D];await B.services.drive_item.save(A)
|
||||
async def prepare_default_drives_for_user(A,user_uid):B=user_uid;await A.get_or_create(user_uid=B,name=_B);await A.get_or_create(user_uid=B,name='Shared Drive');await A.get_or_create(user_uid=B,name=_E);await A.get_or_create(user_uid=B,name=_F);await A.get_or_create(user_uid=B,name=_G);await A.get_or_create(user_uid=B,name=_H)
|
||||
async def prepare_all(A):
|
||||
await A.prepare_default_drives()
|
||||
async for B in A.services.user.find():await A.prepare_default_drives_for_user(B[_D])
|
||||
|
||||
mapper_name = "drive"
|
||||
|
||||
EXTENSIONS_PICTURES = ["jpg", "jpeg", "png", "gif", "svg", "webp", "tiff"]
|
||||
EXTENSIONS_VIDEOS = [
|
||||
"mp4",
|
||||
"m4v",
|
||||
"mov",
|
||||
"wmv",
|
||||
"webm",
|
||||
"mkv",
|
||||
"mpg",
|
||||
"mpeg",
|
||||
"avi",
|
||||
"ogv",
|
||||
"ogg",
|
||||
"flv",
|
||||
"3gp",
|
||||
"3g2",
|
||||
]
|
||||
EXTENSIONS_ARCHIVES = [
|
||||
"zip",
|
||||
"rar",
|
||||
"7z",
|
||||
"tar",
|
||||
"tar.gz",
|
||||
"tar.xz",
|
||||
"tar.bz2",
|
||||
"tar.lzma",
|
||||
"tar.lz",
|
||||
]
|
||||
EXTENSIONS_AUDIO = [
|
||||
"mp3",
|
||||
"wav",
|
||||
"ogg",
|
||||
"flac",
|
||||
"m4a",
|
||||
"wma",
|
||||
"aac",
|
||||
"opus",
|
||||
"aiff",
|
||||
"au",
|
||||
"mid",
|
||||
"midi",
|
||||
]
|
||||
EXTENSIONS_DOCS = [
|
||||
"pdf",
|
||||
"doc",
|
||||
"docx",
|
||||
"xls",
|
||||
"xlsx",
|
||||
"ppt",
|
||||
"pptx",
|
||||
"txt",
|
||||
"md",
|
||||
"json",
|
||||
"csv",
|
||||
"xml",
|
||||
"html",
|
||||
"css",
|
||||
"js",
|
||||
"py",
|
||||
"sql",
|
||||
"rs",
|
||||
"toml",
|
||||
"yml",
|
||||
"yaml",
|
||||
"ini",
|
||||
"conf",
|
||||
"config",
|
||||
"log",
|
||||
"csv",
|
||||
"tsv",
|
||||
"java",
|
||||
"cs",
|
||||
"csproj",
|
||||
"scss",
|
||||
"less",
|
||||
"sass",
|
||||
"json",
|
||||
"lock",
|
||||
"lock.json",
|
||||
"jsonl",
|
||||
]
|
||||
|
||||
async def get_drive_name_by_extension(self, extension):
|
||||
if extension.startswith("."):
|
||||
extension = extension[1:]
|
||||
if extension in self.EXTENSIONS_PICTURES:
|
||||
return "Pictures"
|
||||
if extension in self.EXTENSIONS_VIDEOS:
|
||||
return "Videos"
|
||||
if extension in self.EXTENSIONS_ARCHIVES:
|
||||
return "Archives"
|
||||
if extension in self.EXTENSIONS_AUDIO:
|
||||
return "Audio"
|
||||
if extension in self.EXTENSIONS_DOCS:
|
||||
return "Documents"
|
||||
return "My Drive"
|
||||
|
||||
async def get_drive_by_extension(self, user_uid, extension):
|
||||
name = await self.get_drive_name_by_extension(extension)
|
||||
return await self.get_or_create(user_uid=user_uid, name=name)
|
||||
|
||||
async def get_by_user(self, user_uid, name=None):
|
||||
kwargs = {"user_uid": user_uid}
|
||||
async for model in self.find(**kwargs):
|
||||
if not name:
|
||||
yield model
|
||||
elif model["name"] == name:
|
||||
yield model
|
||||
elif not model["name"] and name == "My Drive":
|
||||
model["name"] = "My Drive"
|
||||
await self.save(model)
|
||||
yield model
|
||||
|
||||
async def get_or_create(self, user_uid, name=None, extensions=None):
|
||||
kwargs = {"user_uid": user_uid}
|
||||
if name:
|
||||
kwargs["name"] = name
|
||||
async for model in self.get_by_user(**kwargs):
|
||||
return model
|
||||
|
||||
model = await self.new()
|
||||
model["user_uid"] = user_uid
|
||||
model["name"] = name
|
||||
await self.save(model)
|
||||
return model
|
||||
|
||||
async def prepare_default_drives(self):
|
||||
async for drive_item in self.services.drive_item.find():
|
||||
extension = drive_item.extension
|
||||
drive = await self.get_drive_by_extension(drive_item["user_uid"], extension)
|
||||
if not drive_item["drive_uid"] == drive["uid"]:
|
||||
drive_item["drive_uid"] = drive["uid"]
|
||||
await self.services.drive_item.save(drive_item)
|
||||
|
||||
async def prepare_default_drives_for_user(self, user_uid):
|
||||
await self.get_or_create(user_uid=user_uid, name="My Drive")
|
||||
await self.get_or_create(user_uid=user_uid, name="Shared Drive")
|
||||
await self.get_or_create(user_uid=user_uid, name="Pictures")
|
||||
await self.get_or_create(user_uid=user_uid, name="Videos")
|
||||
await self.get_or_create(user_uid=user_uid, name="Archives")
|
||||
await self.get_or_create(user_uid=user_uid, name="Documents")
|
||||
|
||||
async def prepare_all(self):
|
||||
await self.prepare_default_drives()
|
||||
async for user in self.services.user.find():
|
||||
await self.prepare_default_drives_for_user(user["uid"])
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
from snek.system.service import BaseService
|
||||
|
||||
|
||||
class DriveItemService(BaseService):
|
||||
mapper_name='drive_item'
|
||||
async def create(B,drive_uid,name,path,type_,size):
|
||||
A=await B.new();A['drive_uid']=drive_uid;A['name']=name;A['path']=str(path);A['extension']=str(name).split('.')[-1];A['file_type']=type_;A['file_size']=size
|
||||
if await B.save(A):return A
|
||||
C=await A.errors;raise Exception(f"Failed to create drive item: {C}.")
|
||||
|
||||
mapper_name = "drive_item"
|
||||
|
||||
async def create(self, drive_uid, name, path, type_, size):
|
||||
model = await self.new()
|
||||
model["drive_uid"] = drive_uid
|
||||
model["name"] = name
|
||||
model["path"] = str(path)
|
||||
model["extension"] = str(name).split(".")[-1]
|
||||
model["file_type"] = type_
|
||||
model["file_size"] = size
|
||||
if await self.save(model):
|
||||
return model
|
||||
errors = await model.errors
|
||||
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.service import BaseService
|
||||
|
||||
|
||||
class NotificationService(BaseService):
|
||||
mapper_name='notification'
|
||||
async def mark_as_read(B,user_uid,channel_message_uid):
|
||||
A=await B.get(user_uid,object_uid=channel_message_uid)
|
||||
if not A:return _B
|
||||
A['read_at']=now();await B.save(A);return True
|
||||
async def get_unread_stats(A,user_uid):await A.query('SELECT object_type, COUNT(*) as count FROM notification WHERE user_uid=:user_uid AND read_at IS NULL GROUP BY object_type',{_A:user_uid})
|
||||
async def create(B,object_uid,object_type,user_uid,message):
|
||||
A=await B.new();A[_C]=object_uid;A[_D]=object_type;A[_A]=user_uid;A[_E]=message
|
||||
if await B.save(A):return A
|
||||
raise Exception(f"Failed to create notification: {A.errors}.")
|
||||
async def create_channel_message(A,channel_message_uid):
|
||||
E=channel_message_uid;D='new_count';F=await A.services.channel_message.get(uid=E);G=await A.services.user.get(uid=F[_A]);A.app.db.begin()
|
||||
async for B in A.services.channel_member.find(channel_uid=F['channel_uid'],is_banned=_B,is_muted=_B,deleted_at=None):
|
||||
if not B[D]:B[D]=0
|
||||
B[D]+=1;H=await A.services.user.get(uid=B[_A])
|
||||
if not H:continue
|
||||
await A.services.channel_member.save(B);C=await A.new();C[_C]=E;C[_D]='channel_message';C[_A]=B[_A];C[_E]=f"New message from {G["nick"]} in {B["label"]}."
|
||||
try:await A.save(C)
|
||||
except Exception:raise Exception(f"Failed to create notification: {C.errors}.")
|
||||
A.app.db.commit()
|
||||
mapper_name = "notification"
|
||||
|
||||
async def mark_as_read(self, user_uid, channel_message_uid):
|
||||
model = await self.get(user_uid, object_uid=channel_message_uid)
|
||||
if not model:
|
||||
return False
|
||||
model["read_at"] = now()
|
||||
await self.save(model)
|
||||
return True
|
||||
|
||||
async def get_unread_stats(self, user_uid):
|
||||
await self.query(
|
||||
"SELECT object_type, COUNT(*) as count FROM notification WHERE user_uid=:user_uid AND read_at IS NULL GROUP BY object_type",
|
||||
{"user_uid": user_uid},
|
||||
)
|
||||
|
||||
async def create(self, object_uid, object_type, user_uid, message):
|
||||
model = await self.new()
|
||||
model["object_uid"] = object_uid
|
||||
model["object_type"] = object_type
|
||||
model["user_uid"] = user_uid
|
||||
model["message"] = message
|
||||
if await self.save(model):
|
||||
return model
|
||||
raise Exception(f"Failed to create notification: {model.errors}.")
|
||||
|
||||
async def create_channel_message(self, channel_message_uid):
|
||||
channel_message = await self.services.channel_message.get(
|
||||
uid=channel_message_uid
|
||||
)
|
||||
user = await self.services.user.get(uid=channel_message["user_uid"])
|
||||
self.app.db.begin()
|
||||
async for channel_member in self.services.channel_member.find(
|
||||
channel_uid=channel_message["channel_uid"],
|
||||
is_banned=False,
|
||||
is_muted=False,
|
||||
deleted_at=None,
|
||||
):
|
||||
if not channel_member["new_count"]:
|
||||
channel_member["new_count"] = 0
|
||||
channel_member["new_count"] += 1
|
||||
|
||||
usr = await self.services.user.get(uid=channel_member["user_uid"])
|
||||
if not usr:
|
||||
continue
|
||||
await self.services.channel_member.save(channel_member)
|
||||
|
||||
model = await self.new()
|
||||
model["object_uid"] = channel_message_uid
|
||||
model["object_type"] = "channel_message"
|
||||
model["user_uid"] = channel_member["user_uid"]
|
||||
model["message"] = (
|
||||
f"New message from {user['nick']} in {channel_member['label']}."
|
||||
)
|
||||
try:
|
||||
await self.save(model)
|
||||
except Exception:
|
||||
raise Exception(f"Failed to create notification: {model.errors}.")
|
||||
|
||||
self.app.db.commit()
|
||||
|
||||
@@ -1,23 +1,52 @@
|
||||
_B='user_uid'
|
||||
_A=False
|
||||
from snek.system.service import BaseService
|
||||
import asyncio,shutil
|
||||
import asyncio
|
||||
import shutil
|
||||
|
||||
class RepositoryService(BaseService):
|
||||
mapper_name='repository'
|
||||
async def delete(B,user_uid,name):
|
||||
A=user_uid;C=asyncio.get_event_loop();D=(await B.services.user.get_repository_path(A)).joinpath(name)
|
||||
try:await C.run_in_executor(None,shutil.rmtree,D)
|
||||
except Exception as E:print(E)
|
||||
await super().delete(user_uid=A,name=name)
|
||||
async def exists(B,user_uid,name,**A):A[_B]=user_uid;A['name']=name;return await super().exists(**A)
|
||||
async def init(D,user_uid,name):
|
||||
B='.git';A=await D.services.user.get_repository_path(user_uid)
|
||||
if not A.exists():A.mkdir(parents=True)
|
||||
A=A.joinpath(name);A=str(A)
|
||||
if not A.endswith(B):A+=B
|
||||
E=['git','init','--bare',A];C=await asyncio.subprocess.create_subprocess_exec(*E,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.PIPE);F,G=await C.communicate();return C.returncode==0
|
||||
async def create(A,user_uid,name,is_private=_A):
|
||||
C=name;D=user_uid
|
||||
if await A.exists(user_uid=D,name=C):return _A
|
||||
if not await A.init(user_uid=D,name=C):return _A
|
||||
B=await A.new();B[_B]=D;B['name']=C;B['is_private']=is_private;return await A.save(B)
|
||||
mapper_name = "repository"
|
||||
|
||||
async def delete(self, user_uid, name):
|
||||
loop = asyncio.get_event_loop()
|
||||
repository_path = (await self.services.user.get_repository_path(user_uid)).joinpath(name)
|
||||
try:
|
||||
await loop.run_in_executor(None, shutil.rmtree, repository_path)
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
|
||||
await super().delete(user_uid=user_uid, name=name)
|
||||
|
||||
|
||||
async def exists(self, user_uid, name, **kwargs):
|
||||
kwargs["user_uid"] = user_uid
|
||||
kwargs["name"] = name
|
||||
return await super().exists(**kwargs)
|
||||
|
||||
async def init(self, user_uid, name):
|
||||
repository_path = await self.services.user.get_repository_path(user_uid)
|
||||
if not repository_path.exists():
|
||||
repository_path.mkdir(parents=True)
|
||||
repository_path = repository_path.joinpath(name)
|
||||
repository_path = str(repository_path)
|
||||
if not repository_path.endswith(".git"):
|
||||
repository_path += ".git"
|
||||
command = ['git', 'init', '--bare', repository_path]
|
||||
process = await asyncio.subprocess.create_subprocess_exec(
|
||||
*command,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
return process.returncode == 0
|
||||
|
||||
async def create(self, user_uid, name,is_private=False):
|
||||
if await self.exists(user_uid=user_uid, name=name):
|
||||
return False
|
||||
|
||||
if not await self.init(user_uid=user_uid, name=name):
|
||||
return False
|
||||
|
||||
model = await self.new()
|
||||
model["user_uid"] = user_uid
|
||||
model["name"] = name
|
||||
model["is_private"] = is_private
|
||||
return await self.save(model)
|
||||
|
||||
+102
-33
@@ -1,36 +1,105 @@
|
||||
_B=False
|
||||
_A=True
|
||||
from snek.model.user import UserModel
|
||||
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 Socket:
|
||||
def __init__(A,ws,user):A.ws=ws;A.is_connected=_A;A.user=user
|
||||
async def send_json(A,data):
|
||||
if not A.is_connected:return _B
|
||||
try:await A.ws.send_json(data)
|
||||
except Exception:A.is_connected=_B
|
||||
return A.is_connected
|
||||
async def close(A):
|
||||
if not A.is_connected:return _A
|
||||
await A.ws.close();A.is_connected=_B;return _A
|
||||
def __init__(A,app):super().__init__(app);A.sockets=set();A.users={};A.subscriptions={}
|
||||
async def add(A,ws,user_uid):
|
||||
B=user_uid;C=A.Socket(ws,await A.app.services.user.get(uid=B));A.sockets.add(C)
|
||||
if not A.users.get(B):A.users[B]=set()
|
||||
A.users[B].add(C)
|
||||
async def subscribe(A,ws,channel_uid,user_uid):
|
||||
B=channel_uid
|
||||
if B not in A.subscriptions:A.subscriptions[B]=set()
|
||||
C=A.Socket(ws,await A.app.services.user.get(uid=user_uid));A.subscriptions[B].add(C)
|
||||
async def send_to_user(B,user_uid,message):
|
||||
A=0
|
||||
for C in B.users.get(user_uid,[]):
|
||||
if await C.send_json(message):A+=1
|
||||
return A
|
||||
async def broadcast(A,channel_uid,message):
|
||||
try:
|
||||
async for B in A.services.channel_member.get_user_uids(channel_uid):print(B,flush=_A);await A.send_to_user(B,message)
|
||||
except Exception as C:print(C,flush=_A)
|
||||
return _A
|
||||
async def delete(A,ws):
|
||||
for B in[A for A in A.sockets if A.ws==ws]:await B.close();A.sockets.remove(B)
|
||||
|
||||
class Socket:
|
||||
def __init__(self, ws, user: UserModel):
|
||||
self.ws = ws
|
||||
self.is_connected = True
|
||||
self.user = user
|
||||
|
||||
|
||||
async def send_json(self, data):
|
||||
if not self.is_connected:
|
||||
return False
|
||||
try:
|
||||
await self.ws.send_json(data)
|
||||
except Exception:
|
||||
self.is_connected = False
|
||||
return self.is_connected
|
||||
|
||||
async def close(self):
|
||||
if not self.is_connected:
|
||||
return True
|
||||
|
||||
await self.ws.close()
|
||||
self.is_connected = False
|
||||
|
||||
return True
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
self.sockets = set()
|
||||
self.users = {}
|
||||
self.subscriptions = {}
|
||||
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
|
||||
|
||||
from snek.system import security
|
||||
from snek.system.service import BaseService
|
||||
|
||||
|
||||
class UserService(BaseService):
|
||||
mapper_name='user'
|
||||
async def get_by_username(A,username):return await A.get(username=username)
|
||||
async def search(C,query,**D):
|
||||
A=query;A=A.strip().lower()
|
||||
if not A:return[]
|
||||
B=[]
|
||||
async for E in C.find(username={'ilike':'%'+A+'%'},**D):B.append(E)
|
||||
return B
|
||||
async def validate_login(C,username,password):
|
||||
A=False;B=await C.get(username=username)
|
||||
if not B:return A
|
||||
if not await security.verify(password,B['password']):return A
|
||||
return _A
|
||||
async def save(B,user):
|
||||
A=user
|
||||
if not A[_B]:A[_B]=await B.services.util.random_light_hex_color()
|
||||
return await super().save(A)
|
||||
async def authenticate(B,username,password):
|
||||
C=password;A=username;print(A,C,flush=_A);D=await B.validate_login(A,C);print(D,flush=_A)
|
||||
if not D:return
|
||||
E=await B.get(username=A,deleted_at=None);return E
|
||||
def get_admin_uids(A):return A.mapper.get_admin_uids()
|
||||
async def get_repository_path(A,user_uid):return pathlib.Path(f"./drive/repositories/{user_uid}")
|
||||
async def get_static_path(B,user_uid):
|
||||
A=pathlib.Path(f"./drive/{user_uid}/snek/static")
|
||||
if not A.exists():return
|
||||
return A
|
||||
async def get_template_path(B,user_uid):
|
||||
A=pathlib.Path(f"./drive/{user_uid}/snek/templates")
|
||||
if not A.exists():return
|
||||
return A
|
||||
async def get_home_folder(B,user_uid):
|
||||
A=pathlib.Path(f"./drive/{user_uid}")
|
||||
if not A.exists():
|
||||
try:A.mkdir(parents=_A,exist_ok=_A)
|
||||
except:pass
|
||||
return A
|
||||
async def register(B,email,username,password):
|
||||
C=username
|
||||
if await B.exists(username=C):raise Exception('User already exists.')
|
||||
A=await B.new();A['nick']=C;A[_B]=await B.services.util.random_light_hex_color();A.email.value=email;A.username.value=C;A.password.value=await security.hash(password)
|
||||
if await B.save(A):
|
||||
if A:
|
||||
D=await B.services.channel.ensure_public_channel(A['uid'])
|
||||
if not D:raise Exception('Failed to create public channel.')
|
||||
return A
|
||||
raise Exception(f"Failed to create user: {A.errors}.")
|
||||
mapper_name = "user"
|
||||
|
||||
async def get_by_username(self, username):
|
||||
return await self.get(username=username)
|
||||
|
||||
async def search(self, query, **kwargs):
|
||||
query = query.strip().lower()
|
||||
if not query:
|
||||
return []
|
||||
results = []
|
||||
async for result in self.find(username={"ilike": "%" + query + "%"}, **kwargs):
|
||||
results.append(result)
|
||||
return results
|
||||
|
||||
async def validate_login(self, username, password):
|
||||
model = await self.get(username=username)
|
||||
if not model:
|
||||
return False
|
||||
if not await security.verify(password, model["password"]):
|
||||
return False
|
||||
return True
|
||||
|
||||
async def save(self, user):
|
||||
if not user["color"]:
|
||||
user["color"] = await self.services.util.random_light_hex_color()
|
||||
return await super().save(user)
|
||||
|
||||
def authenticate_sync(self,username,password):
|
||||
user = self.get_by_username_sync(username)
|
||||
|
||||
if not user:
|
||||
return False
|
||||
if not security.verify_sync(password, user["password"]):
|
||||
return False
|
||||
return True
|
||||
|
||||
async def authenticate(self, username, password):
|
||||
success = await self.validate_login(username, password)
|
||||
if not success:
|
||||
return None
|
||||
|
||||
model = await self.get(username=username, deleted_at=None)
|
||||
return model
|
||||
|
||||
def get_admin_uids(self):
|
||||
return self.mapper.get_admin_uids()
|
||||
|
||||
async def get_repository_path(self, user_uid):
|
||||
return pathlib.Path(f"./drive/repositories/{user_uid}")
|
||||
|
||||
async def get_static_path(self, user_uid):
|
||||
path = pathlib.Path(f"./drive/{user_uid}/snek/static")
|
||||
if not path.exists():
|
||||
return None
|
||||
return path
|
||||
|
||||
|
||||
|
||||
async def get_template_path(self, user_uid):
|
||||
path = pathlib.Path(f"./drive/{user_uid}/snek/templates")
|
||||
if not path.exists():
|
||||
return None
|
||||
return path
|
||||
|
||||
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
|
||||
|
||||
from snek.system.service import BaseService
|
||||
|
||||
|
||||
class UserPropertyService(BaseService):
|
||||
mapper_name=_A
|
||||
async def set(C,user_uid,name,value):A='name';B='user_uid';C.mapper.db[_A].upsert({B:user_uid,A:name,'value':json.dumps(value,default=str)},[B,A])
|
||||
async def get(B,user_uid,name):
|
||||
try:return json.loads((await super().get(user_uid=user_uid,name=name))['value'])
|
||||
except Exception as A:print(A);return
|
||||
async def search(C,query,**D):
|
||||
A=query;A=A.strip().lower()
|
||||
if not A:raise[]
|
||||
B=[]
|
||||
async for E in C.find(name={'ilike':'%'+A+'%'},**D):B.append(E)
|
||||
return B
|
||||
mapper_name = "user_property"
|
||||
|
||||
async def set(self, user_uid, name, value):
|
||||
self.mapper.db["user_property"].upsert(
|
||||
{
|
||||
"user_uid": user_uid,
|
||||
"name": name,
|
||||
"value": json.dumps(value, default=str),
|
||||
},
|
||||
["user_uid", "name"],
|
||||
)
|
||||
|
||||
async def get(self, user_uid, name):
|
||||
try:
|
||||
return json.loads(
|
||||
(await super().get(user_uid=user_uid, name=name))["value"]
|
||||
)
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
return None
|
||||
|
||||
async def search(self, query, **kwargs):
|
||||
query = query.strip().lower()
|
||||
if not query:
|
||||
raise []
|
||||
results = []
|
||||
async for result in self.find(name={"ilike": "%" + query + "%"}, **kwargs):
|
||||
results.append(result)
|
||||
return results
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import random
|
||||
|
||||
from snek.system.service import BaseService
|
||||
|
||||
|
||||
class UtilService(BaseService):
|
||||
async def random_light_hex_color(D):A=random.randint(128,255);B=random.randint(128,255);C=random.randint(128,255);return f"#{A:02x}{B:02x}{C:02x}"
|
||||
|
||||
async def random_light_hex_color(self):
|
||||
|
||||
r = random.randint(128, 255)
|
||||
g = random.randint(128, 255)
|
||||
b = random.randint(128, 255)
|
||||
|
||||
return f"#{r:02x}{g:02x}{b:02x}"
|
||||
|
||||
+484
-205
@@ -1,207 +1,486 @@
|
||||
_O='branches'
|
||||
_N='message'
|
||||
_M='author'
|
||||
_L='Invalid JSON data'
|
||||
_K='origin'
|
||||
_J='Repository not found'
|
||||
_I='main'
|
||||
_H='repository'
|
||||
_G='branch'
|
||||
_F='.git'
|
||||
_E=None
|
||||
_D='user'
|
||||
_C='repo_name'
|
||||
_B='username'
|
||||
_A='repository_path'
|
||||
import os,aiohttp
|
||||
import os
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
import git,shutil,json,tempfile,asyncio,logging,base64,pathlib
|
||||
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger=logging.getLogger('git_server')
|
||||
|
||||
import shutil
|
||||
import json
|
||||
import tempfile
|
||||
import asyncio
|
||||
import logging
|
||||
import base64
|
||||
import pathlib
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger('git_server')
|
||||
|
||||
class GitApplication(web.Application):
|
||||
def __init__(A,parent=_E):B='/branches/{repo_name}';A.parent=parent;super().__init__(client_max_size=5368709120);A.REPO_DIR='drive/repositories/3177f85e-dbb3-4406-993e-3d3748fea545';A.USERS={'x':'x','bob':'bobpass'};A.add_routes([web.post('/create/{repo_name}',A.create_repository),web.delete('/delete/{repo_name}',A.delete_repository),web.get('/clone/{repo_name}',A.clone_repository),web.post('/push/{repo_name}',A.push_repository),web.post('/pull/{repo_name}',A.pull_repository),web.get('/status/{repo_name}',A.status_repository),web.get('/list',A.list_repositories),web.get(B,A.list_branches),web.post(B,A.create_branch),web.get('/log/{repo_name}',A.commit_log),web.get('/file/{repo_name}/{file_path:.*}',A.file_content),web.get('/{path:.+}/info/refs',A.git_smart_http),web.post('/{path:.+}/git-upload-pack',A.git_smart_http),web.post('/{path:.+}/git-receive-pack',A.git_smart_http),web.get('/{repo_name}.git/info/refs',A.git_smart_http),web.post('/{repo_name}.git/git-upload-pack',A.git_smart_http),web.post('/{repo_name}.git/git-receive-pack',A.git_smart_http)])
|
||||
async def check_basic_auth(B,request):
|
||||
C='Basic ';A=request;D=A.headers.get('Authorization','')
|
||||
if not D.startswith(C):return _E,_E
|
||||
E=D.split(C)[1];F=base64.b64decode(E).decode();G,H=F.split(':',1);A[_D]=await B.parent.services.user.authenticate(username=G,password=H)
|
||||
if not A[_D]:return _E,_E
|
||||
A[_A]=await B.parent.services.user.get_repository_path(A[_D]['uid']);return A[_D][_B],A[_A]
|
||||
@staticmethod
|
||||
def require_auth(handler):
|
||||
async def A(self,request,*D,**E):
|
||||
A=request;B,C=await self.check_basic_auth(A)
|
||||
if not B or not C:return web.Response(status=401,headers={'WWW-Authenticate':'Basic'},text='Authentication required')
|
||||
A[_B]=B;A[_A]=C;return await handler(self,A,*D,**E)
|
||||
return A
|
||||
def repo_path(A,repository_path,repo_name):return repository_path.joinpath(repo_name+_F)
|
||||
def check_repo_exists(A,repository_path,repo_name):
|
||||
B=A.repo_path(repository_path,repo_name)
|
||||
if not os.path.exists(B):return web.Response(text=_J,status=404)
|
||||
@require_auth
|
||||
async def create_repository(self,request):
|
||||
B=request;E=B[_B];A=B.match_info[_C];F=B[_A]
|
||||
if not A or'/'in A or'..'in A:return web.Response(text='Invalid repository name',status=400)
|
||||
C=self.repo_path(F,A)
|
||||
if os.path.exists(C):return web.Response(text='Repository already exists',status=400)
|
||||
try:git.Repo.init(C,bare=True);logger.info(f"Created repository: {A} for user {E}");return web.Response(text=f"Created repository {A}")
|
||||
except Exception as D:logger.error(f"Error creating repository {A}: {str(D)}");return web.Response(text=f"Error creating repository: {str(D)}",status=500)
|
||||
@require_auth
|
||||
async def delete_repository(self,request):
|
||||
B=request;F=B[_B];A=B.match_info[_C];C=B[_A];D=self.check_repo_exists(C,A)
|
||||
if D:return D
|
||||
try:shutil.rmtree(self.repo_path(C,A));logger.info(f"Deleted repository: {A} for user {F}");return web.Response(text=f"Deleted repository {A}")
|
||||
except Exception as E:logger.error(f"Error deleting repository {A}: {str(E)}");return web.Response(text=f"Error deleting repository: {str(E)}",status=500)
|
||||
@require_auth
|
||||
async def clone_repository(self,request):
|
||||
A=request;H=A[_B];B=A.match_info[_C];E=A[_A];C=self.check_repo_exists(E,B)
|
||||
if C:return C
|
||||
F=A.host;D=f"http://{F}/{B}.git";G={_H:B,'clone_command':f"git clone {D}",'clone_url':D};return web.json_response(G)
|
||||
@require_auth
|
||||
async def push_repository(self,request):
|
||||
B=request;L=B[_B];C=B.match_info[_C];E=B[_A];F=self.check_repo_exists(E,C)
|
||||
if F:return F
|
||||
try:D=await B.json()
|
||||
except json.JSONDecodeError:return web.Response(text=_L,status=400)
|
||||
M=D.get('commit_message','Update from server');G=D.get(_G,_I);H=D.get('changes',[])
|
||||
if not H:return web.Response(text='No changes provided',status=400)
|
||||
with tempfile.TemporaryDirectory()as I:
|
||||
A=git.Repo.clone_from(self.repo_path(E,C),I)
|
||||
for J in H:
|
||||
K=os.path.join(I,J.get('file',''));N=J.get('content','');os.makedirs(os.path.dirname(K),exist_ok=True)
|
||||
with open(K,'w')as O:O.write(N)
|
||||
A.git.add(A=True)
|
||||
if not A.config_reader().has_section(_D):A.config_writer().set_value(_D,'name','Git Server').release();A.config_writer().set_value(_D,'email','git@server.local').release()
|
||||
A.index.commit(M);P=A.remote(_K);P.push(refspec=f"{G}:{G}")
|
||||
logger.info(f"Pushed to repository: {C} for user {L}");return web.Response(text=f"Successfully pushed changes to {C}")
|
||||
@require_auth
|
||||
async def pull_repository(self,request):
|
||||
C=request;K=C[_B];A=C.match_info[_C];H=C[_A];I=self.check_repo_exists(H,A)
|
||||
if I:return I
|
||||
try:E=await C.json()
|
||||
except json.JSONDecodeError:E={}
|
||||
B=E.get('remote_url');L=E.get(_G,_I)
|
||||
if not B:return web.Response(text='Remote URL is required',status=400)
|
||||
with tempfile.TemporaryDirectory()as M:
|
||||
try:
|
||||
D=git.Repo.clone_from(self.repo_path(H,A),M);F='pull_source'
|
||||
try:G=D.create_remote(F,B)
|
||||
except git.GitCommandError:G=D.remote(F);G.set_url(B)
|
||||
G.fetch();D.git.merge(f"{F}/{L}");N=D.remote(_K);N.push();logger.info(f"Pulled to repository {A} from {B} for user {K}");return web.Response(text=f"Successfully pulled changes from {B} to {A}")
|
||||
except Exception as J:logger.error(f"Error pulling to {A}: {str(J)}");return web.Response(text=f"Error pulling changes: {str(J)}",status=500)
|
||||
@require_auth
|
||||
async def status_repository(self,request):
|
||||
C=request;S=C[_B];B=C.match_info[_C];F=C[_A];G=self.check_repo_exists(F,B)
|
||||
if G:return G
|
||||
with tempfile.TemporaryDirectory()as D:
|
||||
try:
|
||||
E=git.Repo.clone_from(self.repo_path(F,B),D);L=[A.name for A in E.branches];M=E.active_branch.name;H=[]
|
||||
for A in list(E.iter_commits(max_count=5)):H.append({'id':A.hexsha,_M:f"{A.author.name} <{A.author.email}>",'date':A.committed_datetime.isoformat(),_N:A.message})
|
||||
I=[]
|
||||
for(J,T,N)in os.walk(D):
|
||||
if _F in J:continue
|
||||
for O in N:P=os.path.join(J,O);Q=os.path.relpath(P,D);I.append(Q)
|
||||
R={_H:B,_O:L,'active_branch':M,'recent_commits':H,'files':I};return web.json_response(R)
|
||||
except Exception as K:logger.error(f"Error getting status for {B}: {str(K)}");return web.Response(text=f"Error getting repository status: {str(K)}",status=500)
|
||||
@require_auth
|
||||
async def list_repositories(self,request):
|
||||
D=request;G=D[_B]
|
||||
try:
|
||||
A=[];B=self.REPO_DIR
|
||||
if os.path.exists(B):
|
||||
for C in os.listdir(B):
|
||||
F=os.path.join(B,C)
|
||||
if os.path.isdir(F)and C.endswith(_F):A.append(C[:-4])
|
||||
if D.query.get('format')=='json':return web.json_response({'repositories':A})
|
||||
else:return web.Response(text='\n'.join(A)if A else'No repositories found')
|
||||
except Exception as E:logger.error(f"Error listing repositories: {str(E)}");return web.Response(text=f"Error listing repositories: {str(E)}",status=500)
|
||||
@require_auth
|
||||
async def list_branches(self,request):
|
||||
A=request;H=A[_B];B=A.match_info[_C];C=A[_A];D=self.check_repo_exists(C,B)
|
||||
if D:return D
|
||||
with tempfile.TemporaryDirectory()as E:F=git.Repo.clone_from(self.repo_path(C,B),E);G=[A.name for A in F.branches];return web.json_response({_O:G})
|
||||
@require_auth
|
||||
async def create_branch(self,request):
|
||||
B=request;I=B[_B];C=B.match_info[_C];D=B[_A];E=self.check_repo_exists(D,C)
|
||||
if E:return E
|
||||
try:F=await B.json()
|
||||
except json.JSONDecodeError:return web.Response(text=_L,status=400)
|
||||
A=F.get('branch_name');J=F.get('start_point','HEAD')
|
||||
if not A:return web.Response(text='Branch name is required',status=400)
|
||||
with tempfile.TemporaryDirectory()as K:
|
||||
try:G=git.Repo.clone_from(self.repo_path(D,C),K);G.git.branch(A,J);G.git.push(_K,A);logger.info(f"Created branch {A} in repository {C} for user {I}");return web.Response(text=f"Created branch {A}")
|
||||
except Exception as H:logger.error(f"Error creating branch {A} in {C}: {str(H)}");return web.Response(text=f"Error creating branch: {str(H)}",status=500)
|
||||
@require_auth
|
||||
async def commit_log(self,request):
|
||||
B=request;L=B[_B];C=B.match_info[_C];F=B[_A];G=self.check_repo_exists(F,C)
|
||||
if G:return G
|
||||
try:I=int(B.query.get('limit',10));H=B.query.get(_G,_I)
|
||||
except ValueError:return web.Response(text='Invalid limit parameter',status=400)
|
||||
with tempfile.TemporaryDirectory()as J:
|
||||
try:
|
||||
K=git.Repo.clone_from(self.repo_path(F,C),J);E=[]
|
||||
try:
|
||||
for A in list(K.iter_commits(H,max_count=I)):E.append({'id':A.hexsha,'short_id':A.hexsha[:7],_M:f"{A.author.name} <{A.author.email}>",'date':A.committed_datetime.isoformat(),_N:A.message.strip()})
|
||||
except git.GitCommandError as D:
|
||||
if'unknown revision or path'in str(D):E=[]
|
||||
else:raise
|
||||
return web.json_response({_H:C,_G:H,'commits':E})
|
||||
except Exception as D:logger.error(f"Error getting commit log for {C}: {str(D)}");return web.Response(text=f"Error getting commit log: {str(D)}",status=500)
|
||||
@require_auth
|
||||
async def file_content(self,request):
|
||||
A=request;N=A[_B];B=A.match_info[_C];C=A.match_info.get('file_path','');E=A.query.get(_G,_I);F=A[_A];G=self.check_repo_exists(F,B)
|
||||
if G:return G
|
||||
with tempfile.TemporaryDirectory()as H:
|
||||
try:
|
||||
J=git.Repo.clone_from(self.repo_path(F,B),H)
|
||||
try:J.git.checkout(E)
|
||||
except git.GitCommandError:return web.Response(text=f"Branch '{E}' not found",status=404)
|
||||
D=os.path.join(H,C)
|
||||
if not os.path.exists(D):return web.Response(text=f"File '{C}' not found",status=404)
|
||||
if os.path.isdir(D):K=os.listdir(D);return web.json_response({_H:B,'path':C,'type':'directory','contents':K})
|
||||
else:
|
||||
try:
|
||||
with open(D,'r')as L:M=L.read()
|
||||
return web.Response(text=M)
|
||||
except UnicodeDecodeError:return web.Response(text=f"Cannot display binary file content for '{C}'",status=400)
|
||||
except Exception as I:logger.error(f"Error getting file content from {B}: {str(I)}");return web.Response(text=f"Error getting file content: {str(I)}",status=500)
|
||||
@require_auth
|
||||
async def git_smart_http(self,request):
|
||||
B='POST';G='git-receive-pack';H='git-upload-pack';I='Content-Type';J='--stateless-rpc';D='/git-receive-pack';E='/git-upload-pack';F='/info/refs';A=request;P=A[_B];N=A[_A];C=A.path
|
||||
async def K():
|
||||
B=C.lstrip('/')
|
||||
if B.endswith(F):A=B[:-len(F)]
|
||||
elif B.endswith(E):A=B[:-len(E)]
|
||||
elif B.endswith(D):A=B[:-len(D)]
|
||||
else:A=B
|
||||
if A.endswith(_F):A=A[:-4]
|
||||
A=A[4:];G=N.joinpath(A+_F);logger.info(f"Resolved repo path: {G}");return G
|
||||
async def O(service):
|
||||
C=service;D=await K();logger.info(f"handle_info_refs: {D}")
|
||||
if not os.path.exists(D):return web.Response(text=_J,status=404)
|
||||
L=[C,J,'--advertise-refs',str(D)]
|
||||
try:
|
||||
E=await asyncio.create_subprocess_exec(*L,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.PIPE);M,F=await E.communicate()
|
||||
if E.returncode!=0:logger.error(f"Git command failed: {F.decode()}");return web.Response(text=f"Git error: {F.decode()}",status=500)
|
||||
B=web.StreamResponse(status=200,reason='OK',headers={I:f"application/x-{C}-advertisement",'Cache-Control':'no-cache'});await B.prepare(A);G=f"# service={C}\n";N=len(G)+4;O=f"{N:04x}";await B.write(f"{O}{G}0000".encode());await B.write(M);return B
|
||||
except Exception as H:logger.error(f"Error handling info/refs: {str(H)}");return web.Response(text=f"Server error: {str(H)}",status=500)
|
||||
async def L(service):
|
||||
B=service;C=await K();logger.info(f"handle_service_rpc: {C}")
|
||||
if not os.path.exists(C):return web.Response(text=_J,status=404)
|
||||
if not A.headers.get(I)==f"application/x-{B}-request":return web.Response(text='Invalid Content-Type',status=403)
|
||||
G=await A.read();H=[B,J,str(C)]
|
||||
try:
|
||||
D=await asyncio.create_subprocess_exec(*H,stdin=asyncio.subprocess.PIPE,stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.PIPE);L,E=await D.communicate(input=G)
|
||||
if D.returncode!=0:logger.error(f"Git command failed: {E.decode()}");return web.Response(text=f"Git error: {E.decode()}",status=500)
|
||||
return web.Response(body=L,content_type=f"application/x-{B}-result")
|
||||
except Exception as F:logger.error(f"Error handling service RPC: {str(F)}");return web.Response(text=f"Server error: {str(F)}",status=500)
|
||||
if A.method=='GET'and C.endswith(F):
|
||||
M=A.query.get('service')
|
||||
if M in(H,G):return await O(M)
|
||||
else:return web.Response(text='Smart HTTP requires service parameter',status=400)
|
||||
elif A.method==B and E in C:return await L(H)
|
||||
elif A.method==B and D in C:return await L(G)
|
||||
return web.Response(text='Not found',status=404)
|
||||
if __name__=='__main__':
|
||||
try:import uvloop;asyncio.set_event_loop_policy(uvloop.EventLoopPolicy());logger.info('Using uvloop for improved performance')
|
||||
except ImportError:logger.info('uvloop not available, using standard event loop')
|
||||
app=GitApplication();logger.info('Starting Git server on port 8080');web.run_app(app,port=8080)
|
||||
def __init__(self, parent=None):
|
||||
#import git
|
||||
#globals()['git'] = git
|
||||
self.parent = parent
|
||||
super().__init__(client_max_size=1024*1024*1024*5)
|
||||
self.add_routes([
|
||||
web.post('/create/{repo_name}', self.create_repository),
|
||||
web.delete('/delete/{repo_name}', self.delete_repository),
|
||||
web.get('/clone/{repo_name}', self.clone_repository),
|
||||
web.post('/push/{repo_name}', self.push_repository),
|
||||
web.post('/pull/{repo_name}', self.pull_repository),
|
||||
web.get('/status/{repo_name}', self.status_repository),
|
||||
#web.get('/list', self.list_repositories),
|
||||
web.get('/branches/{repo_name}', self.list_branches),
|
||||
web.post('/branches/{repo_name}', self.create_branch),
|
||||
web.get('/log/{repo_name}', self.commit_log),
|
||||
web.get('/file/{repo_name}/{file_path:.*}', self.file_content),
|
||||
web.get('/{path:.+}/info/refs', self.git_smart_http),
|
||||
web.post('/{path:.+}/git-upload-pack', self.git_smart_http),
|
||||
web.post('/{path:.+}/git-receive-pack', self.git_smart_http),
|
||||
web.get('/{repo_name}.git/info/refs', self.git_smart_http),
|
||||
web.post('/{repo_name}.git/git-upload-pack', self.git_smart_http),
|
||||
web.post('/{repo_name}.git/git-receive-pack', self.git_smart_http),
|
||||
])
|
||||
|
||||
|
||||
async def check_basic_auth(self, request):
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if not auth_header.startswith("Basic "):
|
||||
return None,None
|
||||
encoded_creds = auth_header.split("Basic ")[1]
|
||||
decoded_creds = base64.b64decode(encoded_creds).decode()
|
||||
username, password = decoded_creds.split(":", 1)
|
||||
request["user"] = await self.parent.services.user.authenticate(
|
||||
username=username, password=password
|
||||
)
|
||||
if not request["user"]:
|
||||
return None,None
|
||||
request["repository_path"] = await self.parent.services.user.get_repository_path(
|
||||
request["user"]["uid"]
|
||||
)
|
||||
|
||||
return request["user"]['username'],request["repository_path"]
|
||||
|
||||
|
||||
@staticmethod
|
||||
def require_auth(handler):
|
||||
async def wrapped(self, request, *args, **kwargs):
|
||||
username, repository_path = await self.check_basic_auth(request)
|
||||
if not username or not repository_path:
|
||||
return web.Response(status=401, headers={'WWW-Authenticate': 'Basic'}, text='Authentication required')
|
||||
request['username'] = username
|
||||
request['repository_path'] = repository_path
|
||||
return await handler(self, request, *args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
def repo_path(self, repository_path, repo_name):
|
||||
return repository_path.joinpath(repo_name + '.git')
|
||||
|
||||
def check_repo_exists(self, repository_path, repo_name):
|
||||
repo_dir = self.repo_path(repository_path, repo_name)
|
||||
if not os.path.exists(repo_dir):
|
||||
return web.Response(text="Repository not found", status=404)
|
||||
return None
|
||||
|
||||
@require_auth
|
||||
async def create_repository(self, request):
|
||||
username = request['username']
|
||||
repo_name = request.match_info['repo_name']
|
||||
repository_path = request['repository_path']
|
||||
if not repo_name or '/' in repo_name or '..' in repo_name:
|
||||
return web.Response(text="Invalid repository name", status=400)
|
||||
repo_dir = self.repo_path(repository_path, repo_name)
|
||||
if os.path.exists(repo_dir):
|
||||
return web.Response(text="Repository already exists", status=400)
|
||||
try:
|
||||
git.Repo.init(repo_dir, bare=True)
|
||||
logger.info(f"Created repository: {repo_name} for user {username}")
|
||||
return web.Response(text=f"Created repository {repo_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating repository {repo_name}: {str(e)}")
|
||||
return web.Response(text=f"Error creating repository: {str(e)}", status=500)
|
||||
|
||||
@require_auth
|
||||
async def delete_repository(self, request):
|
||||
username = request['username']
|
||||
repo_name = request.match_info['repo_name']
|
||||
repository_path = request['repository_path']
|
||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
||||
if error_response:
|
||||
return error_response
|
||||
#'''
|
||||
try:
|
||||
shutil.rmtree(self.repo_path(repository_path, repo_name))
|
||||
logger.info(f"Deleted repository: {repo_name} for user {username}")
|
||||
return web.Response(text=f"Deleted repository {repo_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting repository {repo_name}: {str(e)}")
|
||||
return web.Response(text=f"Error deleting repository: {str(e)}", status=500)
|
||||
|
||||
@require_auth
|
||||
async def clone_repository(self, request):
|
||||
username = request['username']
|
||||
repo_name = request.match_info['repo_name']
|
||||
repository_path = request['repository_path']
|
||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
||||
if error_response:
|
||||
return error_response
|
||||
host = request.host
|
||||
clone_url = f"http://{host}/{repo_name}.git"
|
||||
response_data = {
|
||||
"repository": repo_name,
|
||||
"clone_command": f"git clone {clone_url}",
|
||||
"clone_url": clone_url
|
||||
}
|
||||
return web.json_response(response_data)
|
||||
|
||||
@require_auth
|
||||
async def push_repository(self, request):
|
||||
username = request['username']
|
||||
repo_name = request.match_info['repo_name']
|
||||
repository_path = request['repository_path']
|
||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
||||
if error_response:
|
||||
return error_response
|
||||
try:
|
||||
data = await request.json()
|
||||
except json.JSONDecodeError:
|
||||
return web.Response(text="Invalid JSON data", status=400)
|
||||
commit_message = data.get('commit_message', 'Update from server')
|
||||
branch = data.get('branch', 'main')
|
||||
changes = data.get('changes', [])
|
||||
if not changes:
|
||||
return web.Response(text="No changes provided", status=400)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
||||
for change in changes:
|
||||
file_path = os.path.join(temp_dir, change.get('file', ''))
|
||||
content = change.get('content', '')
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(content)
|
||||
temp_repo.git.add(A=True)
|
||||
if not temp_repo.config_reader().has_section('user'):
|
||||
temp_repo.config_writer().set_value("user", "name", "Git Server").release()
|
||||
temp_repo.config_writer().set_value("user", "email", "git@server.local").release()
|
||||
temp_repo.index.commit(commit_message)
|
||||
origin = temp_repo.remote('origin')
|
||||
origin.push(refspec=f"{branch}:{branch}")
|
||||
logger.info(f"Pushed to repository: {repo_name} for user {username}")
|
||||
return web.Response(text=f"Successfully pushed changes to {repo_name}")
|
||||
|
||||
@require_auth
|
||||
async def pull_repository(self, request):
|
||||
username = request['username']
|
||||
repo_name = request.match_info['repo_name']
|
||||
repository_path = request['repository_path']
|
||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
||||
if error_response:
|
||||
return error_response
|
||||
try:
|
||||
data = await request.json()
|
||||
except json.JSONDecodeError:
|
||||
data = {}
|
||||
remote_url = data.get('remote_url')
|
||||
branch = data.get('branch', 'main')
|
||||
if not remote_url:
|
||||
return web.Response(text="Remote URL is required", status=400)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
try:
|
||||
local_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
||||
remote_name = "pull_source"
|
||||
try:
|
||||
remote = local_repo.create_remote(remote_name, remote_url)
|
||||
except git.GitCommandError:
|
||||
remote = local_repo.remote(remote_name)
|
||||
remote.set_url(remote_url)
|
||||
remote.fetch()
|
||||
local_repo.git.merge(f"{remote_name}/{branch}")
|
||||
origin = local_repo.remote('origin')
|
||||
origin.push()
|
||||
logger.info(f"Pulled to repository {repo_name} from {remote_url} for user {username}")
|
||||
return web.Response(text=f"Successfully pulled changes from {remote_url} to {repo_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error pulling to {repo_name}: {str(e)}")
|
||||
return web.Response(text=f"Error pulling changes: {str(e)}", status=500)
|
||||
|
||||
@require_auth
|
||||
async def status_repository(self, request):
|
||||
username = request['username']
|
||||
repo_name = request.match_info['repo_name']
|
||||
repository_path = request['repository_path']
|
||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
||||
if error_response:
|
||||
return error_response
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
try:
|
||||
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
||||
branches = [b.name for b in temp_repo.branches]
|
||||
active_branch = temp_repo.active_branch.name
|
||||
commits = []
|
||||
for commit in list(temp_repo.iter_commits(max_count=5)):
|
||||
commits.append({
|
||||
"id": commit.hexsha,
|
||||
"author": f"{commit.author.name} <{commit.author.email}>",
|
||||
"date": commit.committed_datetime.isoformat(),
|
||||
"message": commit.message
|
||||
})
|
||||
files = []
|
||||
for root, dirs, filenames in os.walk(temp_dir):
|
||||
if '.git' in root:
|
||||
continue
|
||||
for filename in filenames:
|
||||
full_path = os.path.join(root, filename)
|
||||
rel_path = os.path.relpath(full_path, temp_dir)
|
||||
files.append(rel_path)
|
||||
status_info = {
|
||||
"repository": repo_name,
|
||||
"branches": branches,
|
||||
"active_branch": active_branch,
|
||||
"recent_commits": commits,
|
||||
"files": files
|
||||
}
|
||||
return web.json_response(status_info)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting status for {repo_name}: {str(e)}")
|
||||
return web.Response(text=f"Error getting repository status: {str(e)}", status=500)
|
||||
|
||||
@require_auth
|
||||
async def list_repositories(self, request):
|
||||
username = request['username']
|
||||
try:
|
||||
repos = []
|
||||
user_dir = self.REPO_DIR
|
||||
if os.path.exists(user_dir):
|
||||
for item in os.listdir(user_dir):
|
||||
item_path = os.path.join(user_dir, item)
|
||||
if os.path.isdir(item_path) and item.endswith('.git'):
|
||||
repos.append(item[:-4])
|
||||
if request.query.get('format') == 'json':
|
||||
return web.json_response({"repositories": repos})
|
||||
else:
|
||||
return web.Response(text="\n".join(repos) if repos else "No repositories found")
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing repositories: {str(e)}")
|
||||
return web.Response(text=f"Error listing repositories: {str(e)}", status=500)
|
||||
|
||||
@require_auth
|
||||
async def list_branches(self, request):
|
||||
username = request['username']
|
||||
repo_name = request.match_info['repo_name']
|
||||
repository_path = request['repository_path']
|
||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
||||
if error_response:
|
||||
return error_response
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
||||
branches = [b.name for b in temp_repo.branches]
|
||||
return web.json_response({"branches": branches})
|
||||
|
||||
@require_auth
|
||||
async def create_branch(self, request):
|
||||
username = request['username']
|
||||
repo_name = request.match_info['repo_name']
|
||||
repository_path = request['repository_path']
|
||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
||||
if error_response:
|
||||
return error_response
|
||||
try:
|
||||
data = await request.json()
|
||||
except json.JSONDecodeError:
|
||||
return web.Response(text="Invalid JSON data", status=400)
|
||||
branch_name = data.get('branch_name')
|
||||
start_point = data.get('start_point', 'HEAD')
|
||||
if not branch_name:
|
||||
return web.Response(text="Branch name is required", status=400)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
try:
|
||||
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
||||
temp_repo.git.branch(branch_name, start_point)
|
||||
temp_repo.git.push('origin', branch_name)
|
||||
logger.info(f"Created branch {branch_name} in repository {repo_name} for user {username}")
|
||||
return web.Response(text=f"Created branch {branch_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating branch {branch_name} in {repo_name}: {str(e)}")
|
||||
return web.Response(text=f"Error creating branch: {str(e)}", status=500)
|
||||
|
||||
@require_auth
|
||||
async def commit_log(self, request):
|
||||
username = request['username']
|
||||
repo_name = request.match_info['repo_name']
|
||||
repository_path = request['repository_path']
|
||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
||||
if error_response:
|
||||
return error_response
|
||||
try:
|
||||
limit = int(request.query.get('limit', 10))
|
||||
branch = request.query.get('branch', 'main')
|
||||
except ValueError:
|
||||
return web.Response(text="Invalid limit parameter", status=400)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
try:
|
||||
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
||||
commits = []
|
||||
try:
|
||||
for commit in list(temp_repo.iter_commits(branch, max_count=limit)):
|
||||
commits.append({
|
||||
"id": commit.hexsha,
|
||||
"short_id": commit.hexsha[:7],
|
||||
"author": f"{commit.author.name} <{commit.author.email}>",
|
||||
"date": commit.committed_datetime.isoformat(),
|
||||
"message": commit.message.strip()
|
||||
})
|
||||
except git.GitCommandError as e:
|
||||
if "unknown revision or path" in str(e):
|
||||
commits = []
|
||||
else:
|
||||
raise
|
||||
return web.json_response({
|
||||
"repository": repo_name,
|
||||
"branch": branch,
|
||||
"commits": commits
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting commit log for {repo_name}: {str(e)}")
|
||||
return web.Response(text=f"Error getting commit log: {str(e)}", status=500)
|
||||
|
||||
@require_auth
|
||||
async def file_content(self, request):
|
||||
username = request['username']
|
||||
repo_name = request.match_info['repo_name']
|
||||
file_path = request.match_info.get('file_path', '')
|
||||
branch = request.query.get('branch', 'main')
|
||||
repository_path = request['repository_path']
|
||||
error_response = self.check_repo_exists(repository_path, repo_name)
|
||||
if error_response:
|
||||
return error_response
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
try:
|
||||
temp_repo = git.Repo.clone_from(self.repo_path(repository_path, repo_name), temp_dir)
|
||||
try:
|
||||
temp_repo.git.checkout(branch)
|
||||
except git.GitCommandError:
|
||||
return web.Response(text=f"Branch '{branch}' not found", status=404)
|
||||
file_full_path = os.path.join(temp_dir, file_path)
|
||||
if not os.path.exists(file_full_path):
|
||||
return web.Response(text=f"File '{file_path}' not found", status=404)
|
||||
if os.path.isdir(file_full_path):
|
||||
files = os.listdir(file_full_path)
|
||||
return web.json_response({
|
||||
"repository": repo_name,
|
||||
"path": file_path,
|
||||
"type": "directory",
|
||||
"contents": files
|
||||
})
|
||||
else:
|
||||
try:
|
||||
with open(file_full_path, 'r') as f:
|
||||
content = f.read()
|
||||
return web.Response(text=content)
|
||||
except UnicodeDecodeError:
|
||||
return web.Response(text=f"Cannot display binary file content for '{file_path}'", status=400)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting file content from {repo_name}: {str(e)}")
|
||||
return web.Response(text=f"Error getting file content: {str(e)}", status=500)
|
||||
|
||||
@require_auth
|
||||
async def git_smart_http(self, request):
|
||||
username = request['username']
|
||||
repository_path = request['repository_path']
|
||||
path = request.path
|
||||
async def get_repository_path():
|
||||
req_path = path.lstrip('/')
|
||||
if req_path.endswith('/info/refs'):
|
||||
repo_name = req_path[:-len('/info/refs')]
|
||||
elif req_path.endswith('/git-upload-pack'):
|
||||
repo_name = req_path[:-len('/git-upload-pack')]
|
||||
elif req_path.endswith('/git-receive-pack'):
|
||||
repo_name = req_path[:-len('/git-receive-pack')]
|
||||
else:
|
||||
repo_name = req_path
|
||||
if repo_name.endswith('.git'):
|
||||
repo_name = repo_name[:-4]
|
||||
repo_name = repo_name[4:]
|
||||
repo_dir = repository_path.joinpath(repo_name + ".git")
|
||||
logger.info(f"Resolved repo path: {repo_dir}")
|
||||
return repo_dir
|
||||
async def handle_info_refs(service):
|
||||
repo_path = await get_repository_path()
|
||||
|
||||
logger.info(f"handle_info_refs: {repo_path}")
|
||||
if not os.path.exists(repo_path):
|
||||
return web.Response(text="Repository not found", status=404)
|
||||
cmd = [service, '--stateless-rpc', '--advertise-refs', str(repo_path)]
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
if process.returncode != 0:
|
||||
logger.error(f"Git command failed: {stderr.decode()}")
|
||||
return web.Response(text=f"Git error: {stderr.decode()}", status=500)
|
||||
response = web.StreamResponse(
|
||||
status=200,
|
||||
reason='OK',
|
||||
headers={
|
||||
'Content-Type': f'application/x-{service}-advertisement',
|
||||
'Cache-Control': 'no-cache'
|
||||
}
|
||||
)
|
||||
await response.prepare(request)
|
||||
packet = f"# service={service}\n"
|
||||
length = len(packet) + 4
|
||||
header = f"{length:04x}"
|
||||
await response.write(f"{header}{packet}0000".encode())
|
||||
await response.write(stdout)
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling info/refs: {str(e)}")
|
||||
return web.Response(text=f"Server error: {str(e)}", status=500)
|
||||
async def handle_service_rpc(service):
|
||||
repo_path = await get_repository_path()
|
||||
logger.info(f"handle_service_rpc: {repo_path}")
|
||||
if not os.path.exists(repo_path):
|
||||
return web.Response(text="Repository not found", status=404)
|
||||
if not request.headers.get('Content-Type') == f'application/x-{service}-request':
|
||||
return web.Response(text="Invalid Content-Type", status=403)
|
||||
body = await request.read()
|
||||
cmd = [service, '--stateless-rpc', str(repo_path)]
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await process.communicate(input=body)
|
||||
if process.returncode != 0:
|
||||
logger.error(f"Git command failed: {stderr.decode()}")
|
||||
return web.Response(text=f"Git error: {stderr.decode()}", status=500)
|
||||
return web.Response(
|
||||
body=stdout,
|
||||
content_type=f'application/x-{service}-result'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling service RPC: {str(e)}")
|
||||
return web.Response(text=f"Server error: {str(e)}", status=500)
|
||||
if request.method == 'GET' and path.endswith('/info/refs'):
|
||||
service = request.query.get('service')
|
||||
if service in ('git-upload-pack', 'git-receive-pack'):
|
||||
return await handle_info_refs(service)
|
||||
else:
|
||||
return web.Response(text="Smart HTTP requires service parameter", status=400)
|
||||
elif request.method == 'POST' and '/git-upload-pack' in path:
|
||||
return await handle_service_rpc('git-upload-pack')
|
||||
elif request.method == 'POST' and '/git-receive-pack' in path:
|
||||
return await handle_service_rpc('git-receive-pack')
|
||||
return web.Response(text="Not found", status=404)
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
import uvloop
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
logger.info("Using uvloop for improved performance")
|
||||
except ImportError:
|
||||
logger.info("uvloop not available, using standard event loop")
|
||||
app = GitApplication()
|
||||
logger.info("Starting Git server on port 8080")
|
||||
web.run_app(app, port=8080)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+223
-191
@@ -1,235 +1,267 @@
|
||||
// Written by retoor@molodetz.nl
|
||||
|
||||
// This project implements a client-server communication system using WebSockets and REST APIs.
|
||||
// This project implements a client-server communication system using WebSockets and REST APIs.
|
||||
// It features a chat system, a notification sound system, and interaction with server endpoints.
|
||||
|
||||
// No additional imports were used beyond standard JavaScript objects and constructors.
|
||||
|
||||
// MIT License
|
||||
|
||||
import { Schedule } from './schedule.js';
|
||||
import { Schedule } from "./schedule.js";
|
||||
import { EventHandler } from "./event-handler.js";
|
||||
import { Socket } from "./socket.js";
|
||||
|
||||
export class RESTClient {
|
||||
debug = false;
|
||||
debug = false;
|
||||
|
||||
async get(url, params = {}) {
|
||||
const encodedParams = new URLSearchParams(params);
|
||||
if (encodedParams) url += '?' + encodedParams;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
if (this.debug) {
|
||||
console.debug({ url, params, result });
|
||||
}
|
||||
return result;
|
||||
async get(url, params = {}) {
|
||||
const encodedParams = new URLSearchParams(params);
|
||||
if (encodedParams) url += "?" + encodedParams;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
if (this.debug) {
|
||||
console.debug({ url, params, result });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async post(url, data) {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
async post(url, data) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (this.debug) {
|
||||
console.debug({ url, data, result });
|
||||
}
|
||||
return result;
|
||||
const result = await response.json();
|
||||
if (this.debug) {
|
||||
console.debug({ url, data, result });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class Chat extends EventHandler {
|
||||
constructor() {
|
||||
super();
|
||||
this._url = window.location.hostname === 'localhost' ? 'ws://localhost/chat.ws' : 'wss://' + window.location.hostname + '/chat.ws';
|
||||
this._socket = null;
|
||||
this._waitConnect = null;
|
||||
this._promises = {};
|
||||
constructor() {
|
||||
super();
|
||||
this._url =
|
||||
window.location.hostname === "localhost"
|
||||
? "ws://localhost/chat.ws"
|
||||
: "wss://" + window.location.hostname + "/chat.ws";
|
||||
this._socket = null;
|
||||
this._waitConnect = null;
|
||||
this._promises = {};
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this._waitConnect) {
|
||||
return this._waitConnect;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this._waitConnect = resolve;
|
||||
console.debug("Connecting..");
|
||||
|
||||
try {
|
||||
this._socket = new WebSocket(this._url);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
setTimeout(() => {
|
||||
this.ensureConnection();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this._waitConnect) {
|
||||
return this._waitConnect;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this._waitConnect = resolve;
|
||||
console.debug("Connecting..");
|
||||
this._socket.onconnect = () => {
|
||||
this._connected();
|
||||
this._waitSocket();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
this._socket = new WebSocket(this._url);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
setTimeout(() => {
|
||||
this.ensureConnection();
|
||||
}, 1000);
|
||||
}
|
||||
generateUniqueId() {
|
||||
return "id-" + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
this._socket.onconnect = () => {
|
||||
this._connected();
|
||||
this._waitSocket();
|
||||
};
|
||||
});
|
||||
}
|
||||
call(method, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const command = { method, args, message_id: this.generateUniqueId() };
|
||||
this._promises[command.message_id] = resolve;
|
||||
this._socket.send(JSON.stringify(command));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateUniqueId() {
|
||||
return 'id-' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
_connected() {
|
||||
this._socket.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.message_id && this._promises[message.message_id]) {
|
||||
this._promises[message.message_id](message);
|
||||
delete this._promises[message.message_id];
|
||||
} else {
|
||||
this.emit("message", message);
|
||||
}
|
||||
};
|
||||
this._socket.onclose = () => {
|
||||
this._waitSocket = null;
|
||||
this._socket = null;
|
||||
this.emit("close");
|
||||
};
|
||||
}
|
||||
|
||||
call(method, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const command = { method, args, message_id: this.generateUniqueId() };
|
||||
this._promises[command.message_id] = resolve;
|
||||
this._socket.send(JSON.stringify(command));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_connected() {
|
||||
this._socket.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.message_id && this._promises[message.message_id]) {
|
||||
this._promises[message.message_id](message);
|
||||
delete this._promises[message.message_id];
|
||||
} else {
|
||||
this.emit("message", message);
|
||||
}
|
||||
};
|
||||
this._socket.onclose = () => {
|
||||
this._waitSocket = null;
|
||||
this._socket = null;
|
||||
this.emit('close');
|
||||
};
|
||||
}
|
||||
|
||||
async privmsg(room, text) {
|
||||
await rest.post("/api/privmsg", {
|
||||
room,
|
||||
text,
|
||||
});
|
||||
}
|
||||
async privmsg(room, text) {
|
||||
await rest.post("/api/privmsg", {
|
||||
room,
|
||||
text,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificationAudio {
|
||||
constructor(timeout = 500) {
|
||||
this.schedule = new Schedule(timeout);
|
||||
}
|
||||
constructor(timeout = 500) {
|
||||
this.schedule = new Schedule(timeout);
|
||||
}
|
||||
|
||||
sounds = {
|
||||
"message": "/audio/soundfx.d_beep3.mp3",
|
||||
"mention": "/audio/750607__deadrobotmusic__notification-sound-1.wav",
|
||||
"messageOtherChannel": "/audio/750608__deadrobotmusic__notification-sound-2.wav",
|
||||
"ping": "/audio/750609__deadrobotmusic__notification-sound-3.wav",
|
||||
}
|
||||
sounds = {
|
||||
message: "/audio/soundfx.d_beep3.mp3",
|
||||
mention: "/audio/750607__deadrobotmusic__notification-sound-1.wav",
|
||||
messageOtherChannel:
|
||||
"/audio/750608__deadrobotmusic__notification-sound-2.wav",
|
||||
ping: "/audio/750609__deadrobotmusic__notification-sound-3.wav",
|
||||
};
|
||||
|
||||
play(soundIndex = 0) {
|
||||
this.schedule.delay(() => {
|
||||
new Audio(this.sounds[soundIndex]).play()
|
||||
.then(() => {
|
||||
console.debug("Gave sound notification");
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Notification failed:", error);
|
||||
});
|
||||
play(soundIndex = 0) {
|
||||
this.schedule.delay(() => {
|
||||
new Audio(this.sounds[soundIndex])
|
||||
.play()
|
||||
.then(() => {
|
||||
console.debug("Gave sound notification");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Notification failed:", error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class App extends EventHandler {
|
||||
rest = new RESTClient();
|
||||
ws = null;
|
||||
rpc = null;
|
||||
audio = null;
|
||||
user = {};
|
||||
|
||||
rest = new RESTClient();
|
||||
ws = null;
|
||||
rpc = null;
|
||||
audio = null;
|
||||
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) {
|
||||
if (this.is_pinging) return false
|
||||
this.is_pinging = true
|
||||
await this.rpc.ping(...args);
|
||||
this.is_pinging = false
|
||||
}
|
||||
if (this.is_pinging) return false;
|
||||
this.is_pinging = true;
|
||||
await this.rpc.ping(...args);
|
||||
this.is_pinging = false;
|
||||
}
|
||||
|
||||
async forcePing(...arg) {
|
||||
await this.rpc.ping(...args);
|
||||
}
|
||||
async forcePing(...arg) {
|
||||
await this.rpc.ping(...args);
|
||||
}
|
||||
starField = null
|
||||
constructor() {
|
||||
super();
|
||||
this.ws = new Socket();
|
||||
this.rpc = this.ws.client;
|
||||
this.audio = new NotificationAudio(500);
|
||||
this.is_pinging = false;
|
||||
this.ping_interval = setInterval(() => {
|
||||
this.ping("active");
|
||||
}, 15000);
|
||||
this.typeEventChannelUid = null;
|
||||
this.typeListener = setInterval(() => {
|
||||
if (this.typeEventChannelUid) {
|
||||
this.rpc.set_typing(this.typeEventChannelUid);
|
||||
this.typeEventChannelUid = null;
|
||||
}
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.ws = new Socket();
|
||||
this.rpc = this.ws.client;
|
||||
this.audio = new NotificationAudio(500);
|
||||
this.is_pinging = false
|
||||
this.ping_interval = setInterval(() => {
|
||||
this.ping("active")
|
||||
}, 15000)
|
||||
|
||||
|
||||
const me = this
|
||||
this.ws.addEventListener("connected", (data) => {
|
||||
this.ping("online")
|
||||
})
|
||||
this.ws.addEventListener("channel-message", (data) => {
|
||||
me.emit("channel-message", data);
|
||||
});
|
||||
|
||||
this.rpc.getUser(null).then(user => {
|
||||
me.user = user;
|
||||
});
|
||||
}
|
||||
|
||||
playSound(index) {
|
||||
this.audio.play(index);
|
||||
}
|
||||
|
||||
timeDescription(isoDate) {
|
||||
const date = new Date(isoDate);
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
|
||||
return timeStr;
|
||||
}
|
||||
|
||||
timeAgo(date1, date2) {
|
||||
const diffMs = Math.abs(date2 - date1);
|
||||
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
||||
|
||||
if (days) {
|
||||
return `${days} ${days > 1 ? 'days' : 'day'} ago`;
|
||||
const me = this;
|
||||
this.ws.addEventListener("connected", (data) => {
|
||||
this.ping("online");
|
||||
});
|
||||
this.ws.addEventListener("reconnecting", (data) => {
|
||||
this.starField?.showNotify("Connecting..","#CC0000")
|
||||
})
|
||||
this.ws.addEventListener("channel-message", (data) => {
|
||||
me.emit("channel-message", data);
|
||||
});
|
||||
this.ws.addEventListener("data", (data) => {
|
||||
if(this._debug){
|
||||
console.debug(data)
|
||||
}
|
||||
if (hours) {
|
||||
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`;
|
||||
}
|
||||
if (minutes) {
|
||||
return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`;
|
||||
}
|
||||
return 'just now';
|
||||
}
|
||||
});
|
||||
this.rpc.getUser(null).then((user) => {
|
||||
me.user = user;
|
||||
});
|
||||
}
|
||||
|
||||
async benchMark(times = 100, message = "Benchmark Message") {
|
||||
const promises = [];
|
||||
const me = this;
|
||||
for (let i = 0; i < times; i++) {
|
||||
promises.push(this.rpc.getChannels().then(channels => {
|
||||
channels.forEach(channel => {
|
||||
me.rpc.sendMessage(channel.uid, `${message} ${i}`);
|
||||
});
|
||||
}));
|
||||
}
|
||||
playSound(index) {
|
||||
this.audio.play(index);
|
||||
}
|
||||
|
||||
timeDescription(isoDate) {
|
||||
const date = new Date(isoDate);
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
|
||||
return timeStr;
|
||||
}
|
||||
|
||||
timeAgo(date1, date2) {
|
||||
const diffMs = Math.abs(date2 - date1);
|
||||
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor(
|
||||
(diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60),
|
||||
);
|
||||
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
||||
|
||||
if (days) {
|
||||
return `${days} ${days > 1 ? "days" : "day"} ago`;
|
||||
}
|
||||
if (hours) {
|
||||
return `${hours} ${hours > 1 ? "hours" : "hour"} ago`;
|
||||
}
|
||||
if (minutes) {
|
||||
return `${minutes} ${minutes > 1 ? "minutes" : "minute"} ago`;
|
||||
}
|
||||
return "just now";
|
||||
}
|
||||
|
||||
async benchMark(times = 100, message = "Benchmark Message") {
|
||||
const promises = [];
|
||||
const me = this;
|
||||
for (let i = 0; i < times; i++) {
|
||||
promises.push(
|
||||
this.rpc.getChannels().then((channels) => {
|
||||
channels.forEach((channel) => {
|
||||
me.rpc.sendMessage(channel.uid, `${message} ${i}`);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const app = new App();
|
||||
|
||||
+245
-69
@@ -5,6 +5,10 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
padding: 50px;
|
||||
height: auto;
|
||||
@@ -25,33 +29,35 @@ body {
|
||||
background-color: #000000;
|
||||
color: #e6e6e6;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"sidebar chat-area";
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
grid-area: chat-area;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #000000;
|
||||
padding-top: 10px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 10px;
|
||||
grid-area: header;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header .logo {
|
||||
color: #fff;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
font-size: 1.2em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -81,13 +87,13 @@ a {
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
color: #f05a28;
|
||||
font-size: 2em;
|
||||
color: #f05a28;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.4em;
|
||||
color: #f05a28;
|
||||
font-size: 1.4em;
|
||||
color: #f05a28;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,13 +101,11 @@ h2 {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #000000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 10px 20px;
|
||||
background-color: #000000;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -112,10 +116,10 @@ h2 {
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
@@ -132,7 +136,11 @@ footer {
|
||||
-ms-overflow-style: none;
|
||||
padding: 10px;
|
||||
height: 10px;
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -152,6 +160,10 @@ footer {
|
||||
|
||||
}
|
||||
|
||||
.chat-messages picture img {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
@@ -228,15 +240,14 @@ footer {
|
||||
|
||||
.chat-input {
|
||||
padding: 15px;
|
||||
background-color: #000000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input[type="text"], .chat-input textarea {
|
||||
flex: 1;
|
||||
background-color: #000000;
|
||||
color: white;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
@@ -273,13 +284,18 @@ input[type="text"], .chat-input textarea {
|
||||
}
|
||||
|
||||
.avatar {
|
||||
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.author, .time {
|
||||
.author {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:not(:hover, :focus-within, :active) .time {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.message.switch-user {
|
||||
@@ -291,6 +307,7 @@ input[type="text"], .chat-input textarea {
|
||||
.avatar {
|
||||
user-select: none;
|
||||
opacity: 1;
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,11 +352,11 @@ a {
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background-color: #000000;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 10px;
|
||||
overflow-y: auto;
|
||||
grid-area: sidebar;
|
||||
}
|
||||
|
||||
.sidebar h2 {
|
||||
@@ -366,46 +384,204 @@ a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
|
||||
header{
|
||||
top: 0;
|
||||
left: 0;
|
||||
text-overflow: ellipsis;
|
||||
width:100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.logo {
|
||||
display:block;
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
}
|
||||
text-align: center;
|
||||
}
|
||||
nav {
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
body {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
header{
|
||||
position: sticky;
|
||||
display: block;
|
||||
.logo {
|
||||
display:block;
|
||||
}
|
||||
}
|
||||
.chat-input {
|
||||
position:sticky;
|
||||
}*/
|
||||
@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) {
|
||||
|
||||
header {
|
||||
top: 0;
|
||||
left: 0;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav {
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
body {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
header{
|
||||
position: sticky;
|
||||
display: block;
|
||||
.logo {
|
||||
display:block;
|
||||
}
|
||||
}
|
||||
.chat-input {
|
||||
position:sticky;
|
||||
}*/
|
||||
}
|
||||
|
||||
dialog {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
background-color: #000; /* Deep black */
|
||||
color: #f1f1f1;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.8);
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
|
||||
animation: dialogFadeIn 0.3s ease-out, dialogScaleIn 0.3s ease-out;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Backdrop styling */
|
||||
dialog::backdrop {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* Title and content */
|
||||
dialog .dialog-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
dialog .dialog-content {
|
||||
font-size: 1rem;
|
||||
color: #ccc;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Button layout */
|
||||
dialog .dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
dialog .dialog-button {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.95rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
|
||||
@keyframes dialogFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dialogScaleIn {
|
||||
from {
|
||||
transform: scale(0.95) translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1) translate(-50%, -50%);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
dialog .dialog-button.primary {
|
||||
background-color: #f05a28;
|
||||
color: white;
|
||||
}
|
||||
|
||||
dialog .dialog-button.primary:hover {
|
||||
background-color: #f05a28;
|
||||
}
|
||||
|
||||
dialog .dialog-button.secondary {
|
||||
background-color: #f0a328;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
dialog .dialog-button.secondary:hover {
|
||||
background-color: #f0b84c;
|
||||
}
|
||||
|
||||
|
||||
.embed-url-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.embed-url-link img,
|
||||
.embed-url-link video,
|
||||
.embed-url-link iframe,
|
||||
.embed-url-link div {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
object-fit: contain;
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
.embed-url-link .page-site {
|
||||
font-size: 0.9em;
|
||||
color: #aaa;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.embed-url-link .page-name {
|
||||
font-size: 1.2em;
|
||||
color: #f05a28;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.embed-url-link .page-description {
|
||||
font-size: 1em;
|
||||
color: #e6e6e6;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.embed-url-link .page-link {
|
||||
font-size: 0.9em;
|
||||
color: #f05a28;
|
||||
text-decoration: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
+355
-51
@@ -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.
|
||||
|
||||
// Includes standard DOM manipulation methods; no external imports used.
|
||||
|
||||
// MIT License: This code is open-source and can be reused and distributed under the terms of the MIT License.
|
||||
|
||||
class ChatInputElement extends HTMLElement {
|
||||
_chatWindow = null
|
||||
class ChatInputComponent extends HTMLElement {
|
||||
autoCompletions = {
|
||||
"example 1": () => {},
|
||||
"example 2": () => {},
|
||||
}
|
||||
hiddenCompletions = {
|
||||
"/starsRender": () => {
|
||||
app.rpc.starsRender(this.channelUid,this.value.replace("/starsRender ",""))
|
||||
}
|
||||
}
|
||||
users = []
|
||||
textarea = null
|
||||
_value = ""
|
||||
lastUpdateEvent = null
|
||||
previousValue = ""
|
||||
lastChange = null
|
||||
changed = false
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.component = document.createElement('div');
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
this.lastUpdateEvent = new Date();
|
||||
this.textarea = document.createElement("textarea");
|
||||
this.value = this.getAttribute("value") || "";
|
||||
this.previousValue = this.value;
|
||||
this.lastChange = new Date();
|
||||
this.changed = false;
|
||||
}
|
||||
set chatWindow(value){
|
||||
this._chatWindow = value
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
get chatWindow(){
|
||||
return this._chatWindow
|
||||
}
|
||||
get channelUid() {
|
||||
return this.chatWindow.channel.uid
|
||||
}
|
||||
connectedCallback() {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '/base.css';
|
||||
this.component.appendChild(link);
|
||||
|
||||
this.container = document.createElement('div');
|
||||
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;
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
this.textBox.addEventListener('change', (e) => {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));
|
||||
console.error(e.target.value);
|
||||
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.textBox.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
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();
|
||||
const message = e.target.value.trim();
|
||||
if (!message) return;
|
||||
this.dispatchEvent(new CustomEvent('submit', { detail: message, bubbles: true }));
|
||||
e.target.value = '';
|
||||
autoCompletion = this.resolveAutoComplete();
|
||||
if (autoCompletion) {
|
||||
e.target.value = autoCompletion;
|
||||
this.value = autoCompletion;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
|
||||
const message = me.replaceMentionsWithAuthors(this.value);
|
||||
e.target.value = "";
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
let autoCompletionHandler = this.allAutoCompletions[this.value.split(" ")[0]];
|
||||
if (autoCompletionHandler) {
|
||||
autoCompletionHandler();
|
||||
this.value = "";
|
||||
this.previousValue = "";
|
||||
e.target.value = "";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateMessage()
|
||||
app.rpc.finalizeMessage(this.messageUid)
|
||||
this.value = "";
|
||||
this.previousValue = "";
|
||||
this.messageUid = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.component.appendChild(this.container);
|
||||
this.changeInterval = setInterval(() => {
|
||||
if (!this.liveType) {
|
||||
return;
|
||||
}
|
||||
if (this.value !== this.previousValue) {
|
||||
if (
|
||||
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
|
||||
this.liveTypeInterval
|
||||
) {
|
||||
this.value = "";
|
||||
this.previousValue = "";
|
||||
}
|
||||
this.lastChange = new Date();
|
||||
}
|
||||
this.update();
|
||||
}, 300);
|
||||
|
||||
this.addEventListener("upload", (e) => {
|
||||
this.focus();
|
||||
});
|
||||
this.addEventListener("uploaded", function (e) {
|
||||
let message = "";
|
||||
e.detail.files.forEach((file) => {
|
||||
message += `[${file.name}](/channel/attachment/${file.relative_url})`;
|
||||
});
|
||||
app.rpc.sendMessage(this.channelUid, message,true);
|
||||
});
|
||||
setTimeout(()=>{
|
||||
this.focus();
|
||||
},1000)
|
||||
}
|
||||
|
||||
trackSecondsBetweenEvents(event1Time, event2Time) {
|
||||
const millisecondsDifference = event2Time.getTime() - event1Time.getTime();
|
||||
return millisecondsDifference / 1000;
|
||||
}
|
||||
isSubsequence(s, t) {
|
||||
let i = 0, j = 0;
|
||||
while (i < s.length && j < t.length) {
|
||||
if (s[i] === t[j]) {
|
||||
i++;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
return i === s.length;
|
||||
}
|
||||
|
||||
|
||||
newMessage() {
|
||||
if (!this.messageUid) {
|
||||
this.messageUid = "?";
|
||||
}
|
||||
|
||||
this.value = this.replaceMentionsWithAuthors(this.value);
|
||||
this.sendMessage(this.channelUid, this.value,!this.liveType).then((uid) => {
|
||||
if (this.liveType) {
|
||||
this.messageUid = uid;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateMessage() {
|
||||
if (this.value[0] == "/") {
|
||||
return false;
|
||||
}
|
||||
if (!this.messageUid) {
|
||||
this.newMessage();
|
||||
return false;
|
||||
}
|
||||
if (this.messageUid === "?") {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
typeof app !== "undefined" &&
|
||||
app.rpc &&
|
||||
typeof app.rpc.updateMessageText === "function"
|
||||
) {
|
||||
app.rpc.updateMessageText(this.messageUid, this.replaceMentionsWithAuthors(this.value));
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus() {
|
||||
if (this.liveType) {
|
||||
return;
|
||||
}
|
||||
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) {
|
||||
this.lastUpdateEvent = new Date();
|
||||
if (
|
||||
typeof app !== "undefined" &&
|
||||
app.rpc &&
|
||||
typeof app.rpc.set_typing === "function"
|
||||
) {
|
||||
app.rpc.set_typing(this.channelUid, this.user.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
const expired =
|
||||
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
|
||||
this.liveTypeInterval;
|
||||
const changed = this.value !== this.previousValue;
|
||||
|
||||
if (changed || expired) {
|
||||
this.lastChange = new Date();
|
||||
this.updateStatus();
|
||||
}
|
||||
|
||||
this.previousValue = this.value;
|
||||
|
||||
if (this.liveType && expired) {
|
||||
this.value = "";
|
||||
this.previousValue = "";
|
||||
this.messageUid = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
if (this.liveType) {
|
||||
this.updateMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(channelUid, value,is_final) {
|
||||
if (!value.trim()) {
|
||||
return null;
|
||||
}
|
||||
return await app.rpc.sendMessage(channelUid, value,is_final);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chat-input', ChatInputElement);
|
||||
customElements.define("chat-input", ChatInputComponent);
|
||||
|
||||
@@ -6,77 +6,77 @@
|
||||
|
||||
// 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.
|
||||
|
||||
class ChatWindowElement extends HTMLElement {
|
||||
receivedHistory = false;
|
||||
channel = null
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.component = document.createElement('section');
|
||||
this.app = app;
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
receivedHistory = false;
|
||||
channel = null;
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.component = document.createElement("section");
|
||||
this.app = app;
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
|
||||
get user() {
|
||||
return this.app.user;
|
||||
}
|
||||
get user() {
|
||||
return this.app.user;
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '/base.css';
|
||||
this.component.appendChild(link);
|
||||
this.component.classList.add("chat-area");
|
||||
|
||||
this.container = document.createElement("section");
|
||||
this.container.classList.add("chat-area", "chat-window");
|
||||
async connectedCallback() {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = "/base.css";
|
||||
this.component.appendChild(link);
|
||||
this.component.classList.add("chat-area");
|
||||
|
||||
const chatHeader = document.createElement("div");
|
||||
chatHeader.classList.add("chat-header");
|
||||
this.container = document.createElement("section");
|
||||
this.container.classList.add("chat-area", "chat-window");
|
||||
|
||||
const chatTitle = document.createElement('h2');
|
||||
chatTitle.classList.add("chat-title");
|
||||
chatTitle.classList.add("no-select");
|
||||
chatTitle.innerText = "Loading...";
|
||||
chatHeader.appendChild(chatTitle);
|
||||
this.container.appendChild(chatHeader);
|
||||
const chatHeader = document.createElement("div");
|
||||
chatHeader.classList.add("chat-header");
|
||||
|
||||
const channels = await app.rpc.getChannels();
|
||||
const channel = channels[0];
|
||||
this.channel = channel;
|
||||
chatTitle.innerText = channel.name;
|
||||
const chatTitle = document.createElement("h2");
|
||||
chatTitle.classList.add("chat-title");
|
||||
chatTitle.classList.add("no-select");
|
||||
chatTitle.innerText = "Loading...";
|
||||
chatHeader.appendChild(chatTitle);
|
||||
this.container.appendChild(chatHeader);
|
||||
|
||||
const channelElement = document.createElement('message-list');
|
||||
channelElement.setAttribute("channel", channel.uid);
|
||||
this.container.appendChild(channelElement);
|
||||
const channels = await app.rpc.getChannels();
|
||||
const channel = channels[0];
|
||||
this.channel = channel;
|
||||
chatTitle.innerText = channel.name;
|
||||
|
||||
const chatInput = document.createElement('chat-input');
|
||||
chatInput.chatWindow = this;
|
||||
chatInput.addEventListener("submit", (e) => {
|
||||
app.rpc.sendMessage(channel.uid, e.detail);
|
||||
});
|
||||
this.container.appendChild(chatInput);
|
||||
const channelElement = document.createElement("message-list");
|
||||
channelElement.setAttribute("channel", channel.uid);
|
||||
this.container.appendChild(channelElement);
|
||||
|
||||
this.component.appendChild(this.container);
|
||||
const chatInput = document.createElement("chat-input");
|
||||
chatInput.chatWindow = this;
|
||||
chatInput.addEventListener("submit", (e) => {
|
||||
app.rpc.sendMessage(channel.uid, e.detail);
|
||||
});
|
||||
this.container.appendChild(chatInput);
|
||||
|
||||
const messages = await app.rpc.getMessages(channel.uid);
|
||||
messages.forEach(message => {
|
||||
if (!message['user_nick']) return;
|
||||
channelElement.addMessage(message);
|
||||
});
|
||||
this.component.appendChild(this.container);
|
||||
|
||||
const me = this;
|
||||
channelElement.addEventListener("message", (message) => {
|
||||
if (me.user.uid !== message.detail.user_uid) app.playSound(0);
|
||||
|
||||
message.detail.element.scrollIntoView({"block": "end"});
|
||||
});
|
||||
}
|
||||
const messages = await app.rpc.getMessages(channel.uid);
|
||||
messages.forEach((message) => {
|
||||
if (!message["user_nick"]) return;
|
||||
channelElement.addMessage(message);
|
||||
});
|
||||
|
||||
const me = this;
|
||||
channelElement.addEventListener("message", (message) => {
|
||||
if (me.user.uid !== message.detail.user_uid) app.playSound(0);
|
||||
|
||||
message.detail.element.scrollIntoView({ block: "end" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chat-window', ChatWindowElement);
|
||||
customElements.define("chat-window", ChatWindowElement);
|
||||
|
||||
@@ -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,16 +1,15 @@
|
||||
|
||||
|
||||
export class EventHandler {
|
||||
constructor() {
|
||||
this.subscribers = {};
|
||||
}
|
||||
constructor() {
|
||||
this.subscribers = {};
|
||||
}
|
||||
|
||||
addEventListener(type, handler) {
|
||||
if (!this.subscribers[type]) this.subscribers[type] = [];
|
||||
this.subscribers[type].push(handler);
|
||||
}
|
||||
addEventListener(type, handler) {
|
||||
if (!this.subscribers[type]) this.subscribers[type] = [];
|
||||
this.subscribers[type].push(handler);
|
||||
}
|
||||
|
||||
emit(type, ...data) {
|
||||
if (this.subscribers[type]) this.subscribers[type].forEach(handler => handler(...data));
|
||||
}
|
||||
}
|
||||
emit(type, ...data) {
|
||||
if (this.subscribers[type])
|
||||
this.subscribers[type].forEach((handler) => handler(...data));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,28 +2,25 @@
|
||||
|
||||
// This JavaScript class defines a custom HTML element <fancy-button>, which creates a styled, clickable button element with customizable size, text, and URL redirect functionality.
|
||||
|
||||
|
||||
// MIT License
|
||||
|
||||
|
||||
|
||||
class FancyButton extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.url = null;
|
||||
this.type = "button";
|
||||
this.value = null;
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.url = null;
|
||||
this.type = "button";
|
||||
this.value = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.container = document.createElement('span');
|
||||
let size = this.getAttribute('size');
|
||||
console.info({ GG: size });
|
||||
size = size === 'auto' ? '1%' : '33%';
|
||||
connectedCallback() {
|
||||
this.container = document.createElement("span");
|
||||
let size = this.getAttribute("size");
|
||||
console.info({ GG: size });
|
||||
size = size === "auto" ? "1%" : "33%";
|
||||
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.innerHTML = `
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.innerHTML = `
|
||||
:root {
|
||||
width: 100%;
|
||||
--width: 100%;
|
||||
@@ -49,29 +46,30 @@ class FancyButton extends HTMLElement {
|
||||
}
|
||||
`;
|
||||
|
||||
this.container.appendChild(this.styleElement);
|
||||
this.buttonElement = document.createElement('button');
|
||||
this.container.appendChild(this.buttonElement);
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
this.container.appendChild(this.styleElement);
|
||||
this.buttonElement = document.createElement("button");
|
||||
this.container.appendChild(this.buttonElement);
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
|
||||
this.url = this.getAttribute('url');
|
||||
|
||||
this.url = this.getAttribute("url");
|
||||
|
||||
this.value = this.getAttribute('value');
|
||||
this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text")));
|
||||
this.buttonElement.addEventListener("click", () => {
|
||||
if(this.url == 'submit'){
|
||||
this.closest('form').submit()
|
||||
return
|
||||
}
|
||||
|
||||
if (this.url === "/back" || this.url === "/back/") {
|
||||
window.history.back();
|
||||
} else if (this.url) {
|
||||
window.location = this.url;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.value = this.getAttribute("value");
|
||||
this.buttonElement.appendChild(
|
||||
document.createTextNode(this.getAttribute("text")),
|
||||
);
|
||||
this.buttonElement.addEventListener("click", () => {
|
||||
if (this.url == "submit") {
|
||||
this.closest("form").submit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.url === "/back" || this.url === "/back/") {
|
||||
window.history.back();
|
||||
} else if (this.url) {
|
||||
window.location = this.url;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("fancy-button", FancyButton);
|
||||
|
||||
@@ -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) {
|
||||
val = val ?? '';
|
||||
val = val ?? "";
|
||||
this.inputElement.value = val;
|
||||
this.inputElement.setAttribute("value", val);
|
||||
}
|
||||
@@ -62,9 +62,9 @@ class GenericField extends HTMLElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({mode: 'open'});
|
||||
this.container = document.createElement('div');
|
||||
this.styleElement = document.createElement('style');
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.container = document.createElement("div");
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.innerHTML = `
|
||||
|
||||
h1 {
|
||||
@@ -174,7 +174,7 @@ class GenericField extends HTMLElement {
|
||||
|
||||
if (this.inputElement == null && this.field) {
|
||||
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.inputElement.name = this.field.name;
|
||||
@@ -182,26 +182,39 @@ class GenericField extends HTMLElement {
|
||||
|
||||
const me = this;
|
||||
this.inputElement.addEventListener("keyup", (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const event = new CustomEvent("change", {detail: me, bubbles: true});
|
||||
if (e.key === "Enter") {
|
||||
const event = new CustomEvent("change", {
|
||||
detail: me,
|
||||
bubbles: true,
|
||||
});
|
||||
me.dispatchEvent(event);
|
||||
|
||||
me.dispatchEvent(new Event("submit"));
|
||||
} 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);
|
||||
}
|
||||
});
|
||||
|
||||
this.inputElement.addEventListener("click", (e) => {
|
||||
const event = new CustomEvent("click", {detail: me, bubbles: true});
|
||||
const event = new CustomEvent("click", { detail: me, bubbles: true });
|
||||
me.dispatchEvent(event);
|
||||
});
|
||||
|
||||
this.inputElement.addEventListener("blur", (e) => {
|
||||
const event = new CustomEvent("change", {detail: me, bubbles: true});
|
||||
me.dispatchEvent(event);
|
||||
}, true);
|
||||
this.inputElement.addEventListener(
|
||||
"blur",
|
||||
(e) => {
|
||||
const event = new CustomEvent("change", {
|
||||
detail: me,
|
||||
bubbles: true,
|
||||
});
|
||||
me.dispatchEvent(event);
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
this.container.appendChild(this.inputElement);
|
||||
}
|
||||
@@ -210,8 +223,8 @@ class GenericField extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.inputElement.setAttribute("type", this.field.type ?? 'input');
|
||||
this.inputElement.setAttribute("name", this.field.name ?? '');
|
||||
this.inputElement.setAttribute("type", this.field.type ?? "input");
|
||||
this.inputElement.setAttribute("name", this.field.name ?? "");
|
||||
|
||||
if (this.field.text != null) {
|
||||
this.inputElement.innerText = this.field.text;
|
||||
@@ -239,14 +252,14 @@ class GenericField extends HTMLElement {
|
||||
this.inputElement.removeAttribute("required");
|
||||
}
|
||||
if (!this.footerElement) {
|
||||
this.footerElement = document.createElement('div');
|
||||
this.footerElement.style.clear = 'both';
|
||||
this.footerElement = document.createElement("div");
|
||||
this.footerElement.style.clear = "both";
|
||||
this.container.appendChild(this.footerElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('generic-field', GenericField);
|
||||
customElements.define("generic-field", GenericField);
|
||||
|
||||
class GenericForm extends HTMLElement {
|
||||
fields = {};
|
||||
@@ -254,7 +267,7 @@ class GenericForm extends HTMLElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({mode: 'open'});
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.styleElement = document.createElement("style");
|
||||
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.classList.add("generic-form-container");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const preloadedForm = this.getAttribute('preloaded-structure');
|
||||
const preloadedForm = this.getAttribute("preloaded-structure");
|
||||
if (preloadedForm) {
|
||||
try {
|
||||
const form = JSON.parse(preloadedForm);
|
||||
this.constructForm(form)
|
||||
this.constructForm(form);
|
||||
} catch (error) {
|
||||
console.error(error, preloadedForm);
|
||||
}
|
||||
}
|
||||
const url = this.getAttribute('url');
|
||||
const url = this.getAttribute("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("/")) {
|
||||
fullUrl.searchParams.set('url', url);
|
||||
fullUrl.searchParams.set("url", url);
|
||||
}
|
||||
this.loadForm(fullUrl.toString());
|
||||
} else {
|
||||
@@ -318,10 +333,10 @@ class GenericForm extends HTMLElement {
|
||||
let hasAutoFocus = Object.keys(this.fields).length !== 0;
|
||||
|
||||
fields.sort((a, b) => a.index - b.index);
|
||||
fields.forEach(field => {
|
||||
const updatingField = field.name in this.fields
|
||||
fields.forEach((field) => {
|
||||
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];
|
||||
|
||||
@@ -362,7 +377,7 @@ class GenericForm extends HTMLElement {
|
||||
window.location.pathname = saveResult.redirect_url;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -374,7 +389,9 @@ class GenericForm extends HTMLElement {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
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());
|
||||
@@ -387,15 +404,15 @@ class GenericForm extends HTMLElement {
|
||||
const url = this.getAttribute("url");
|
||||
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
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();
|
||||
Object.values(form.fields).forEach(field => {
|
||||
Object.values(form.fields).forEach((field) => {
|
||||
if (!this.form.fields[field.name]) {
|
||||
return;
|
||||
}
|
||||
@@ -409,23 +426,23 @@ class GenericForm extends HTMLElement {
|
||||
this.fields[field.name].setAttribute("field", field);
|
||||
this.fields[field.name].updateAttributes();
|
||||
});
|
||||
Object.values(form.fields).forEach(field => {
|
||||
Object.values(form.fields).forEach((field) => {
|
||||
this.fields[field.name].setErrors(field.errors);
|
||||
});
|
||||
return form['is_valid'];
|
||||
return form["is_valid"];
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const url = this.getAttribute("url");
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('generic-form', GenericForm);
|
||||
customElements.define("generic-form", GenericForm);
|
||||
|
||||
@@ -7,48 +7,50 @@
|
||||
// MIT License
|
||||
|
||||
class HTMLFrame extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.container = document.createElement('div');
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.container = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.container.classList.add("html_frame");
|
||||
let url = this.getAttribute('url');
|
||||
if (!url.startsWith("https")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
if (url) {
|
||||
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get");
|
||||
if (!url.startsWith("/")) {
|
||||
fullUrl.searchParams.set('url', url);
|
||||
}
|
||||
this.loadAndRender(fullUrl.toString());
|
||||
} else {
|
||||
this.container.textContent = "No source URL!";
|
||||
}
|
||||
connectedCallback() {
|
||||
this.container.classList.add("html_frame");
|
||||
let url = this.getAttribute("url");
|
||||
if (!url.startsWith("https")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
if (url) {
|
||||
const fullUrl = url.startsWith("/")
|
||||
? window.location.origin + url
|
||||
: new URL(window.location.origin + "/http-get");
|
||||
if (!url.startsWith("/")) {
|
||||
fullUrl.searchParams.set("url", url);
|
||||
}
|
||||
this.loadAndRender(fullUrl.toString());
|
||||
} else {
|
||||
this.container.textContent = "No source URL!";
|
||||
}
|
||||
}
|
||||
|
||||
async loadAndRender(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const html = await response.text();
|
||||
if (url.endsWith(".md")) {
|
||||
const markdownElement = document.createElement('div');
|
||||
markdownElement.innerHTML = html;
|
||||
this.outerHTML = html;
|
||||
} else {
|
||||
this.container.innerHTML = html;
|
||||
}
|
||||
} catch (error) {
|
||||
this.container.textContent = `Error: ${error.message}`;
|
||||
}
|
||||
async loadAndRender(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const html = await response.text();
|
||||
if (url.endsWith(".md")) {
|
||||
const markdownElement = document.createElement("div");
|
||||
markdownElement.innerHTML = html;
|
||||
this.outerHTML = html;
|
||||
} else {
|
||||
this.container.innerHTML = html;
|
||||
}
|
||||
} catch (error) {
|
||||
this.container.textContent = `Error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('html-frame', HTMLFrame);
|
||||
customElements.define("html-frame", HTMLFrame);
|
||||
|
||||
@@ -12,22 +12,22 @@
|
||||
class HTMLFrame extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.container = document.createElement('div');
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.container = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.container.classList.add('html_frame');
|
||||
const url = this.getAttribute('url');
|
||||
this.container.classList.add("html_frame");
|
||||
const url = this.getAttribute("url");
|
||||
if (url) {
|
||||
const fullUrl = url.startsWith('/')
|
||||
const fullUrl = url.startsWith("/")
|
||||
? window.location.origin + url
|
||||
: new URL(window.location.origin + '/http-get');
|
||||
if (!url.startsWith('/')) fullUrl.searchParams.set('url', url);
|
||||
: new URL(window.location.origin + "/http-get");
|
||||
if (!url.startsWith("/")) fullUrl.searchParams.set("url", url);
|
||||
this.loadAndRender(fullUrl.toString());
|
||||
} else {
|
||||
this.container.textContent = 'No source URL!';
|
||||
this.container.textContent = "No source URL!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,4 @@ class HTMLFrame extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('markdown-frame', HTMLFrame);
|
||||
customElements.define("markdown-frame", HTMLFrame);
|
||||
|
||||
+98
-100
@@ -4,7 +4,6 @@
|
||||
|
||||
// No external libraries or dependencies are used other than standard web components.
|
||||
|
||||
|
||||
// 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:
|
||||
@@ -13,20 +12,19 @@
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
class TileGridElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.gridId = this.getAttribute('grid');
|
||||
this.component = document.createElement('div');
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.gridId = this.getAttribute("grid");
|
||||
this.component = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('connected');
|
||||
this.styleElement = document.createElement('style');
|
||||
this.styleElement.textContent = `
|
||||
connectedCallback() {
|
||||
console.log("connected");
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.textContent = `
|
||||
.grid {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
@@ -47,53 +45,53 @@ class TileGridElement extends HTMLElement {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
`;
|
||||
this.component.appendChild(this.styleElement);
|
||||
this.container = document.createElement('div');
|
||||
this.container.classList.add('gallery');
|
||||
this.component.appendChild(this.container);
|
||||
}
|
||||
this.component.appendChild(this.styleElement);
|
||||
this.container = document.createElement("div");
|
||||
this.container.classList.add("gallery");
|
||||
this.component.appendChild(this.container);
|
||||
}
|
||||
|
||||
addImage(src) {
|
||||
const item = document.createElement('img');
|
||||
item.src = src;
|
||||
item.classList.add('tile');
|
||||
item.style.width = '100px';
|
||||
item.style.height = '100px';
|
||||
this.container.appendChild(item);
|
||||
}
|
||||
addImage(src) {
|
||||
const item = document.createElement("img");
|
||||
item.src = src;
|
||||
item.classList.add("tile");
|
||||
item.style.width = "100px";
|
||||
item.style.height = "100px";
|
||||
this.container.appendChild(item);
|
||||
}
|
||||
|
||||
addImages(srcs) {
|
||||
srcs.forEach(src => this.addImage(src));
|
||||
}
|
||||
addImages(srcs) {
|
||||
srcs.forEach((src) => this.addImage(src));
|
||||
}
|
||||
|
||||
addElement(element) {
|
||||
element.classList.add('tile');
|
||||
this.container.appendChild(element);
|
||||
}
|
||||
addElement(element) {
|
||||
element.classList.add("tile");
|
||||
this.container.appendChild(element);
|
||||
}
|
||||
}
|
||||
|
||||
class UploadButton extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.component = document.createElement('div');
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
window.u = this;
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.component = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
window.u = this;
|
||||
}
|
||||
|
||||
get gridSelector() {
|
||||
return this.getAttribute('grid');
|
||||
}
|
||||
grid = null;
|
||||
get gridSelector() {
|
||||
return this.getAttribute("grid");
|
||||
}
|
||||
grid = null;
|
||||
|
||||
addImages(urls) {
|
||||
this.grid.addImages(urls);
|
||||
}
|
||||
addImages(urls) {
|
||||
this.grid.addImages(urls);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('connected');
|
||||
this.styleElement = document.createElement('style');
|
||||
this.styleElement.textContent = `
|
||||
connectedCallback() {
|
||||
console.log("connected");
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.textContent = `
|
||||
.upload-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -115,61 +113,61 @@ class UploadButton extends HTMLElement {
|
||||
background-color: #999;
|
||||
}
|
||||
`;
|
||||
this.shadowRoot.appendChild(this.styleElement);
|
||||
this.container = document.createElement('div');
|
||||
this.container.classList.add('upload-button');
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.multiple = true;
|
||||
input.addEventListener('change', (e) => {
|
||||
const files = e.target.files;
|
||||
const urls = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
urls.push(e.target.result);
|
||||
if (urls.length === files.length) {
|
||||
this.addImages(urls);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(files[i]);
|
||||
}
|
||||
});
|
||||
const label = document.createElement('label');
|
||||
label.textContent = 'Upload Images';
|
||||
label.appendChild(input);
|
||||
this.container.appendChild(label);
|
||||
}
|
||||
this.shadowRoot.appendChild(this.styleElement);
|
||||
this.container = document.createElement("div");
|
||||
this.container.classList.add("upload-button");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.multiple = true;
|
||||
input.addEventListener("change", (e) => {
|
||||
const files = e.target.files;
|
||||
const urls = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
urls.push(e.target.result);
|
||||
if (urls.length === files.length) {
|
||||
this.addImages(urls);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(files[i]);
|
||||
}
|
||||
});
|
||||
const label = document.createElement("label");
|
||||
label.textContent = "Upload Images";
|
||||
label.appendChild(input);
|
||||
this.container.appendChild(label);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('upload-button', UploadButton);
|
||||
customElements.define('tile-grid', TileGridElement);
|
||||
customElements.define("upload-button", UploadButton);
|
||||
customElements.define("tile-grid", TileGridElement);
|
||||
|
||||
class MeniaUploadElement extends HTMLElement {
|
||||
constructor(){
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.component = document.createElement("div");
|
||||
alert('aaaa');
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.component = document.createElement("div");
|
||||
alert("aaaa");
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.container = document.createElement("div");
|
||||
this.component.style.height = '100%';
|
||||
this.component.style.backgroundColor = 'blue';
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
connectedCallback() {
|
||||
this.container = document.createElement("div");
|
||||
this.component.style.height = "100%";
|
||||
this.component.style.backgroundColor = "blue";
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
|
||||
this.tileElement = document.createElement("tile-grid");
|
||||
this.tileElement.style.backgroundColor = 'red';
|
||||
this.tileElement.style.height = '100%';
|
||||
this.component.appendChild(this.tileElement);
|
||||
this.tileElement = document.createElement("tile-grid");
|
||||
this.tileElement.style.backgroundColor = "red";
|
||||
this.tileElement.style.height = "100%";
|
||||
this.component.appendChild(this.tileElement);
|
||||
|
||||
this.uploadButton = document.createElement('upload-button');
|
||||
this.component.appendChild(this.uploadButton);
|
||||
}
|
||||
this.uploadButton = document.createElement("upload-button");
|
||||
this.component.appendChild(this.uploadButton);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('menia-upload', MeniaUploadElement);
|
||||
customElements.define("menia-upload", MeniaUploadElement);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// This JavaScript source code defines a custom HTML element named "message-list-manager" to manage a list of message lists for different channels obtained asynchronously.
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
// MIT License
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
@@ -22,23 +22,22 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
|
||||
class MessageListManagerElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.container = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.container = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
const channels = await app.rpc.getChannels();
|
||||
channels.forEach(channel => {
|
||||
const messageList = document.createElement("message-list");
|
||||
messageList.setAttribute("channel", channel.uid);
|
||||
this.container.appendChild(messageList);
|
||||
});
|
||||
}
|
||||
async connectedCallback() {
|
||||
const channels = await app.rpc.getChannels();
|
||||
channels.forEach((channel) => {
|
||||
const messageList = document.createElement("message-list");
|
||||
messageList.setAttribute("channel", channel.uid);
|
||||
this.container.appendChild(messageList);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("message-list-manager", MessageListManagerElement);
|
||||
customElements.define("message-list-manager", MessageListManagerElement);
|
||||
|
||||
+93
-156
@@ -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.
|
||||
|
||||
// MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty.
|
||||
import { app } from "../app.js";
|
||||
class MessageList extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
app.ws.addEventListener("update_message_text", (data) => {
|
||||
this.updateMessageText(data.uid, data);
|
||||
});
|
||||
app.ws.addEventListener("set_typing", (data) => {
|
||||
this.triggerGlow(data.user_uid,data.color);
|
||||
});
|
||||
|
||||
class MessageListElement extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ["messages"];
|
||||
this.items = [];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const messagesContainer = this
|
||||
messagesContainer.addEventListener('click', (e) => {
|
||||
if (e.target.tagName !== 'IMG' || e.target.classList.contains('avatar-img')) return;
|
||||
const img = e.target;
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;'
|
||||
const fullImg = document.createElement('img');
|
||||
const urlObj = new URL(img.src); urlObj.search = '';
|
||||
fullImg.src = urlObj.toString();
|
||||
fullImg.alt = img.alt;
|
||||
fullImg.style.maxWidth = '90%';
|
||||
fullImg.style.maxHeight = '90%';
|
||||
overlay.appendChild(fullImg);
|
||||
document.body.appendChild(overlay);
|
||||
overlay.addEventListener('click', () => document.body.removeChild(overlay));
|
||||
})
|
||||
|
||||
}
|
||||
isElementVisible(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
}
|
||||
isScrolledToBottom() {
|
||||
return this.isElementVisible(this.querySelector(".message-list-bottom"));
|
||||
}
|
||||
scrollToBottom(force) {
|
||||
this.scrollTop = this.scrollHeight;
|
||||
|
||||
this.querySelector(".message-list-bottom").scrollIntoView();
|
||||
setTimeout(() => {
|
||||
|
||||
this.scrollTop = this.scrollHeight;
|
||||
this.querySelector(".message-list-bottom").scrollIntoView();
|
||||
},200)
|
||||
}
|
||||
updateMessageText(uid, message) {
|
||||
const messageDiv = this.querySelector('div[data-uid="' + uid + '"]');
|
||||
|
||||
if (!messageDiv) {
|
||||
return;
|
||||
}
|
||||
|
||||
messages = [];
|
||||
room = null;
|
||||
url = null;
|
||||
container = null;
|
||||
messageEventSchedule = null;
|
||||
observer = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.component = document.createElement('div');
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
const scrollToBottom = this.isScrolledToBottom();
|
||||
const receivedHtml = document.createElement("div");
|
||||
receivedHtml.innerHTML = message.html;
|
||||
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;
|
||||
}
|
||||
});
|
||||
if (lastElement) {
|
||||
lastElement.classList.add("glow");
|
||||
setTimeout(() => {
|
||||
lastElement.classList.remove("glow");
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
linkifyText(text) {
|
||||
const urlRegex = /https?:\/\/[^\s]+/g;
|
||||
return text.replace(urlRegex, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
|
||||
}
|
||||
set data(items) {
|
||||
this.items = items;
|
||||
this.render();
|
||||
}
|
||||
render() {
|
||||
this.innerHTML = "";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
scrollBottom() {
|
||||
this.container.scrollTop = this.container.scrollHeight;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '/base.css';
|
||||
this.component.appendChild(link);
|
||||
this.component.classList.add("chat-messages");
|
||||
|
||||
this.container = document.createElement('div');
|
||||
this.component.appendChild(this.container);
|
||||
|
||||
this.messageEventSchedule = new Schedule(500);
|
||||
this.messages = [];
|
||||
this.channel_uid = this.getAttribute("channel");
|
||||
|
||||
app.addEventListener(this.channel_uid, (data) => {
|
||||
this.addMessage(data);
|
||||
});
|
||||
|
||||
this.dispatchEvent(new CustomEvent("rendered", { detail: this, bubbles: true }));
|
||||
|
||||
this.timeUpdateInterval = setInterval(() => {
|
||||
this.messages.forEach((message) => {
|
||||
const newText = this.timeDescription(message.created_at);
|
||||
if (newText != message.element.innerText) {
|
||||
message.element.querySelector(".time").innerText = newText;
|
||||
}
|
||||
});
|
||||
}, 30000);
|
||||
}
|
||||
//this.insertAdjacentHTML("beforeend", html);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('message-list', MessageListElement);
|
||||
customElements.define("message-list", MessageList);
|
||||
|
||||
+24
-14
@@ -7,20 +7,30 @@
|
||||
// MIT License
|
||||
|
||||
class MessageModel {
|
||||
constructor(uid, channel_uid, user_uid, user_nick, color, message, html, created_at, updated_at) {
|
||||
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
|
||||
}
|
||||
constructor(
|
||||
uid,
|
||||
channel_uid,
|
||||
user_uid,
|
||||
user_nick,
|
||||
color,
|
||||
message,
|
||||
html,
|
||||
created_at,
|
||||
updated_at,
|
||||
) {
|
||||
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 = {
|
||||
Message: MessageModel
|
||||
}
|
||||
Message: MessageModel,
|
||||
};
|
||||
|
||||
+32
-28
@@ -1,30 +1,34 @@
|
||||
this.onpush = (event) => {
|
||||
console.log(event.data);
|
||||
// From here we can write the data to IndexedDB, send it to any open
|
||||
// windows, display a notification, etc.
|
||||
};
|
||||
console.log(event.data);
|
||||
// From here we can write the data to IndexedDB, send it to any open
|
||||
// windows, display a notification, etc.
|
||||
};
|
||||
|
||||
navigator.serviceWorker
|
||||
.register("/service-worker.js")
|
||||
.then((serviceWorkerRegistration) => {
|
||||
serviceWorkerRegistration.pushManager.subscribe().then(
|
||||
(pushSubscription) => {
|
||||
const subscriptionObject = {
|
||||
endpoint: pushSubscription.endpoint,
|
||||
keys: {
|
||||
p256dh: pushSubscription.getKey('p256dh'),
|
||||
auth: pushSubscription.getKey('auth'),
|
||||
},
|
||||
encoding: PushManager.supportedContentEncodings,
|
||||
/* other app-specific data, such as user identity */
|
||||
};
|
||||
console.log(pushSubscription.endpoint, pushSubscription, subscriptionObject);
|
||||
// The push subscription details needed by the application
|
||||
// server are now available, and can be sent to it using,
|
||||
// for example, the fetch() API.
|
||||
},
|
||||
(error) => {
|
||||
console.error(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
navigator.serviceWorker
|
||||
.register("/service-worker.js")
|
||||
.then((serviceWorkerRegistration) => {
|
||||
serviceWorkerRegistration.pushManager.subscribe().then(
|
||||
(pushSubscription) => {
|
||||
const subscriptionObject = {
|
||||
endpoint: pushSubscription.endpoint,
|
||||
keys: {
|
||||
p256dh: pushSubscription.getKey("p256dh"),
|
||||
auth: pushSubscription.getKey("auth"),
|
||||
},
|
||||
encoding: PushManager.supportedContentEncodings,
|
||||
/* other app-specific data, such as user identity */
|
||||
};
|
||||
console.log(
|
||||
pushSubscription.endpoint,
|
||||
pushSubscription,
|
||||
subscriptionObject,
|
||||
);
|
||||
// The push subscription details needed by the application
|
||||
// server are now available, and can be sent to it using,
|
||||
// for example, the fetch() API.
|
||||
},
|
||||
(error) => {
|
||||
console.error(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -51,4 +51,4 @@ export class Schedule {
|
||||
me.timeOutCount = 0;
|
||||
}, this.msDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
async function requestNotificationPermission() {
|
||||
const permission = await Notification.requestPermission();
|
||||
return permission === 'granted';
|
||||
return permission === "granted";
|
||||
}
|
||||
|
||||
// Subscribe to Push Notifications
|
||||
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({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
|
||||
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY),
|
||||
});
|
||||
|
||||
// Send subscription to your backend
|
||||
await fetch('/subscribe', {
|
||||
method: 'POST',
|
||||
await fetch("/subscribe", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(subscription),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Service Worker (service-worker.js)
|
||||
self.addEventListener('push', event => {
|
||||
self.addEventListener("push", (event) => {
|
||||
const data = event.data.json();
|
||||
self.registration.showNotification(data.title, {
|
||||
body: data.message,
|
||||
icon: data.icon
|
||||
icon: data.icon,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+131
-117
@@ -1,137 +1,151 @@
|
||||
import {EventHandler} from "./event-handler.js";
|
||||
import { EventHandler } from "./event-handler.js";
|
||||
|
||||
export class Socket extends EventHandler {
|
||||
/**
|
||||
* @type {URL}
|
||||
*/
|
||||
url
|
||||
/**
|
||||
* @type {WebSocket|null}
|
||||
*/
|
||||
ws = null
|
||||
/**
|
||||
* @type {URL}
|
||||
*/
|
||||
url;
|
||||
/**
|
||||
* @type {WebSocket|null}
|
||||
*/
|
||||
ws = null;
|
||||
|
||||
/**
|
||||
* @type {null|PromiseWithResolvers<Socket>&{resolved?:boolean}}
|
||||
*/
|
||||
connection = null
|
||||
/**
|
||||
* @type {null|PromiseWithResolvers<Socket>&{resolved?:boolean}}
|
||||
*/
|
||||
connection = null;
|
||||
|
||||
shouldReconnect = true;
|
||||
shouldReconnect = true;
|
||||
|
||||
get isConnected() {
|
||||
return this.ws && this.ws.readyState === WebSocket.OPEN;
|
||||
_debug = false;
|
||||
|
||||
get isConnected() {
|
||||
return this.ws && this.ws.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
get isConnecting() {
|
||||
return this.ws && this.ws.readyState === WebSocket.CONNECTING;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.url = new URL("/rpc.ws", window.location.origin);
|
||||
this.url.protocol = this.url.protocol.replace("http", "ws");
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.ws) {
|
||||
return this.connection.promise;
|
||||
}
|
||||
|
||||
get isConnecting() {
|
||||
return this.ws && this.ws.readyState === WebSocket.CONNECTING;
|
||||
if (!this.connection || this.connection.resolved) {
|
||||
this.connection = Promise.withResolvers();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.ws = new WebSocket(this.url);
|
||||
this.ws.addEventListener("open", () => {
|
||||
this.connection.resolved = true;
|
||||
this.connection.resolve(this);
|
||||
this.emit("connected");
|
||||
});
|
||||
|
||||
this.url = new URL('/rpc.ws', window.location.origin);
|
||||
this.url.protocol = this.url.protocol.replace('http', 'ws');
|
||||
|
||||
this.connect()
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.ws) {
|
||||
return this.connection.promise;
|
||||
this.ws.addEventListener("close", () => {
|
||||
console.log("Connection closed");
|
||||
this.disconnect();
|
||||
});
|
||||
this.ws.addEventListener("error", (e) => {
|
||||
console.error("Connection error", e);
|
||||
this.disconnect();
|
||||
});
|
||||
this.ws.addEventListener("message", (e) => {
|
||||
if (e.data instanceof Blob || e.data instanceof ArrayBuffer) {
|
||||
console.error("Binary data not supported");
|
||||
} else {
|
||||
try {
|
||||
this.onData(JSON.parse(e.data));
|
||||
} catch (e) {
|
||||
console.error("Failed to parse message", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.connection || this.connection.resolved) {
|
||||
this.connection = Promise.withResolvers()
|
||||
}
|
||||
|
||||
this.ws = new WebSocket(this.url);
|
||||
this.ws.addEventListener("open", () => {
|
||||
this.connection.resolved = true;
|
||||
this.connection.resolve(this);
|
||||
this.emit("connected");
|
||||
});
|
||||
|
||||
this.ws.addEventListener("close", () => {
|
||||
console.log("Connection closed");
|
||||
this.disconnect()
|
||||
})
|
||||
this.ws.addEventListener("error", (e) => {
|
||||
console.error("Connection error", e);
|
||||
this.disconnect()
|
||||
})
|
||||
this.ws.addEventListener("message", (e) => {
|
||||
if (e.data instanceof Blob || e.data instanceof ArrayBuffer) {
|
||||
console.error("Binary data not supported");
|
||||
} else {
|
||||
try {
|
||||
this.onData(JSON.parse(e.data));
|
||||
} catch (e) {
|
||||
console.error("Failed to parse message", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
onData(data) {
|
||||
if (data.success !== undefined && !data.success) {
|
||||
console.error(data);
|
||||
}
|
||||
|
||||
|
||||
onData(data) {
|
||||
if (data.success !== undefined && !data.success) {
|
||||
console.error(data);
|
||||
}
|
||||
if (data.callId) {
|
||||
this.emit(data.callId, data.data);
|
||||
}
|
||||
if (data.channel_uid) {
|
||||
this.emit(data.channel_uid, data.data);
|
||||
this.emit("channel-message", data);
|
||||
}
|
||||
if (data.callId) {
|
||||
this.emit(data.callId, data.data);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.ws?.close();
|
||||
this.ws = null;
|
||||
|
||||
if (this.shouldReconnect) setTimeout(() => {
|
||||
console.log("Reconnecting");
|
||||
return this.connect();
|
||||
}, 0);
|
||||
if (data.channel_uid) {
|
||||
this.emit(data.channel_uid, data.data);
|
||||
if (!data["event"]) this.emit("channel-message", data);
|
||||
}
|
||||
|
||||
|
||||
_camelToSnake(str) {
|
||||
return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
||||
this.emit("data", data.data);
|
||||
if (data["event"]) {
|
||||
this.emit(data.event, data.data);
|
||||
}
|
||||
}
|
||||
|
||||
get client() {
|
||||
const me = this;
|
||||
return new Proxy({}, {
|
||||
get(_, prop) {
|
||||
return (...args) => {
|
||||
const functionName = me._camelToSnake(prop);
|
||||
return me.call(functionName, ...args);
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
disconnect() {
|
||||
this.ws?.close();
|
||||
this.ws = null;
|
||||
|
||||
generateCallId() {
|
||||
return self.crypto.randomUUID();
|
||||
}
|
||||
if (this.shouldReconnect)
|
||||
setTimeout(() => {
|
||||
console.log("Reconnecting");
|
||||
this.emit("reconnecting");
|
||||
return this.connect();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
async sendJson(data) {
|
||||
await this.connect().then(api => {
|
||||
api.ws.send(JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
_camelToSnake(str) {
|
||||
return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
||||
}
|
||||
|
||||
async call(method, ...args) {
|
||||
const call = {
|
||||
callId: this.generateCallId(),
|
||||
method,
|
||||
args,
|
||||
};
|
||||
const me = this
|
||||
return new Promise((resolve) => {
|
||||
me.addEventListener(call.callId, data => resolve(data));
|
||||
me.sendJson(call);
|
||||
});
|
||||
}
|
||||
}
|
||||
get client() {
|
||||
const me = this;
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, prop) {
|
||||
return (...args) => {
|
||||
const functionName = me._camelToSnake(prop);
|
||||
if(me._debug){
|
||||
const call = {}
|
||||
call[functionName] = args
|
||||
console.debug(call)
|
||||
}
|
||||
return me.call(functionName, ...args);
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
generateCallId() {
|
||||
return self.crypto.randomUUID();
|
||||
}
|
||||
|
||||
async sendJson(data) {
|
||||
await this.connect().then((api) => {
|
||||
api.ws.send(JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
|
||||
async call(method, ...args) {
|
||||
const call = {
|
||||
callId: this.generateCallId(),
|
||||
method,
|
||||
args,
|
||||
};
|
||||
const me = this;
|
||||
return new Promise((resolve) => {
|
||||
me.addEventListener(call.callId, (data) => resolve(data));
|
||||
me.sendJson(call);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,60 +2,62 @@
|
||||
|
||||
// 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:
|
||||
|
||||
class UploadButtonElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
}
|
||||
chatInput = null;
|
||||
async uploadFiles() {
|
||||
const fileInput = this.container.querySelector(".file-input");
|
||||
const uploadButton = this.container.querySelector(".upload-button");
|
||||
|
||||
if (!fileInput.files.length) {
|
||||
return;
|
||||
}
|
||||
chatInput = null
|
||||
async uploadFiles() {
|
||||
const fileInput = this.container.querySelector('.file-input');
|
||||
const uploadButton = this.container.querySelector('.upload-button');
|
||||
|
||||
if (!fileInput.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = fileInput.files;
|
||||
const formData = new FormData();
|
||||
formData.append('channel_uid', this.channelUid);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append('files[]', files[i]);
|
||||
}
|
||||
|
||||
const request = new XMLHttpRequest();
|
||||
request.open('POST', '/drive.bin', true);
|
||||
|
||||
request.upload.onprogress = function (event) {
|
||||
if (event.lengthComputable) {
|
||||
const percentComplete = (event.loaded / event.total) * 100;
|
||||
uploadButton.innerText = `${Math.round(percentComplete)}%`;
|
||||
}
|
||||
};
|
||||
|
||||
request.onload = function () {
|
||||
if (request.status === 200) {
|
||||
uploadButton.innerHTML = '📤';
|
||||
} else {
|
||||
alert('Upload failed');
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = function () {
|
||||
alert('Error while uploading.');
|
||||
};
|
||||
|
||||
request.send(formData);
|
||||
const uploadEvent = new Event('upload',{});
|
||||
this.dispatchEvent(uploadEvent);
|
||||
const files = fileInput.files;
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append("files[]", files[i]);
|
||||
}
|
||||
channelUid = null
|
||||
connectedCallback() {
|
||||
this.styleElement = document.createElement('style');
|
||||
this.styleElement.innerHTML = `
|
||||
const request = new XMLHttpRequest();
|
||||
|
||||
request.responseType = "json";
|
||||
request.open("POST", `/channel/${this.channelUid}/attachment.bin`, true);
|
||||
|
||||
request.upload.onprogress = function (event) {
|
||||
if (event.lengthComputable) {
|
||||
const percentComplete = (event.loaded / event.total) * 100;
|
||||
uploadButton.innerText = `${Math.round(percentComplete)}%`;
|
||||
}
|
||||
};
|
||||
const me = this;
|
||||
request.onload = function () {
|
||||
if (request.status === 200) {
|
||||
me.dispatchEvent(
|
||||
new CustomEvent("uploaded", { detail: request.response }),
|
||||
);
|
||||
uploadButton.innerHTML = "📤";
|
||||
} else {
|
||||
alert("Upload failed");
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = function () {
|
||||
alert("Error while uploading.");
|
||||
};
|
||||
|
||||
request.send(formData);
|
||||
const uploadEvent = new Event("upload", {});
|
||||
this.dispatchEvent(uploadEvent);
|
||||
}
|
||||
channelUid = null;
|
||||
connectedCallback() {
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.innerHTML = `
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
@@ -96,9 +98,9 @@ class UploadButtonElement extends HTMLElement {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
this.shadowRoot.appendChild(this.styleElement);
|
||||
this.container = document.createElement('div');
|
||||
this.container.innerHTML = `
|
||||
this.shadowRoot.appendChild(this.styleElement);
|
||||
this.container = document.createElement("div");
|
||||
this.container.innerHTML = `
|
||||
<div class="upload-container">
|
||||
<button class="upload-button">
|
||||
📤
|
||||
@@ -106,17 +108,17 @@ class UploadButtonElement extends HTMLElement {
|
||||
<input class="hidden-input file-input" type="file" multiple />
|
||||
</div>
|
||||
`;
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
this.channelUid = this.getAttribute('channel');
|
||||
this.uploadButton = this.container.querySelector('.upload-button');
|
||||
this.fileInput = this.container.querySelector('.hidden-input');
|
||||
this.uploadButton.addEventListener('click', () => {
|
||||
this.fileInput.click();
|
||||
});
|
||||
this.fileInput.addEventListener('change', () => {
|
||||
this.uploadFiles();
|
||||
});
|
||||
}
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
this.channelUid = this.getAttribute("channel");
|
||||
this.uploadButton = this.container.querySelector(".upload-button");
|
||||
this.fileInput = this.container.querySelector(".hidden-input");
|
||||
this.uploadButton.addEventListener("click", () => {
|
||||
this.fileInput.click();
|
||||
});
|
||||
this.fileInput.addEventListener("change", () => {
|
||||
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)
|
||||
|
||||
+148
-64
@@ -1,67 +1,151 @@
|
||||
_C='delete'
|
||||
_B='set'
|
||||
_A='get'
|
||||
import functools,json
|
||||
import functools
|
||||
import json
|
||||
|
||||
from snek.system import security
|
||||
cache=functools.cache
|
||||
CACHE_MAX_ITEMS_DEFAULT=5000
|
||||
|
||||
cache = functools.cache
|
||||
|
||||
CACHE_MAX_ITEMS_DEFAULT = 5000
|
||||
|
||||
|
||||
class Cache:
|
||||
def __init__(A,app,max_items=CACHE_MAX_ITEMS_DEFAULT):A.app=app;A.cache={};A.max_items=max_items;A.stats={};A.lru=[];A.version=15505
|
||||
async def get(A,args):
|
||||
B=args;await A.update_stat(B,_A)
|
||||
try:A.lru.pop(A.lru.index(B))
|
||||
except:return
|
||||
A.lru.insert(0,B)
|
||||
while len(A.lru)>A.max_items:A.cache.pop(A.lru[-1]);A.lru.pop()
|
||||
return A.cache[B]
|
||||
async def get_stats(A):
|
||||
C=[]
|
||||
for B in A.lru:C.append({'key':B,_B:A.stats[B][_B],_A:A.stats[B][_A],_C:A.stats[B][_C],'value':str(A.serialize(A.cache[B].record))})
|
||||
return C
|
||||
def serialize(C,obj):B=None;A=obj.copy();A.pop('created_at',B);A.pop('deleted_at',B);A.pop('email',B);A.pop('password',B);return A
|
||||
async def update_stat(A,key,action):
|
||||
C=action;B=key
|
||||
if B not in A.stats:A.stats[B]={_B:0,_A:0,_C:0}
|
||||
A.stats[B][C]=A.stats[B][C]+1
|
||||
def json_default(B,value):
|
||||
A=value
|
||||
try:return json.dumps(A.__dict__,default=str)
|
||||
except:return str(A)
|
||||
async def create_cache_key(A,args,kwargs):return await security.hash(json.dumps({'args':args,'kwargs':kwargs},sort_keys=True,default=A.json_default))
|
||||
async def set(A,args,result):
|
||||
B=args;C=B not in A.cache;A.cache[B]=result;await A.update_stat(B,_B)
|
||||
try:A.lru.pop(A.lru.index(B))
|
||||
except(ValueError,IndexError):pass
|
||||
A.lru.insert(0,B)
|
||||
while len(A.lru)>A.max_items:A.cache.pop(A.lru[-1]);A.lru.pop()
|
||||
if C:A.version+=1
|
||||
async def delete(A,args):
|
||||
B=args;await A.update_stat(B,_C)
|
||||
if B in A.cache:
|
||||
try:A.lru.pop(A.lru.index(B))
|
||||
except IndexError:pass
|
||||
del A.cache[B]
|
||||
def async_cache(A,func):
|
||||
@functools.wraps(func)
|
||||
async def B(*B,**C):
|
||||
D=await A.create_cache_key(B,C);E=await A.get(D)
|
||||
if E:return E
|
||||
F=await func(*B,**C);await A.set(D,F);return F
|
||||
return B
|
||||
def async_delete_cache(A,func):
|
||||
@functools.wraps(func)
|
||||
async def B(*C,**D):
|
||||
B=await A.create_cache_key(C,D)
|
||||
if B in A.cache:
|
||||
try:A.lru.pop(A.lru.index(B))
|
||||
except IndexError:pass
|
||||
del A.cache[B]
|
||||
return await func(*C,**D)
|
||||
return B
|
||||
def __init__(self, app, max_items=CACHE_MAX_ITEMS_DEFAULT):
|
||||
self.app = app
|
||||
self.cache = {}
|
||||
self.max_items = max_items
|
||||
self.stats = {}
|
||||
self.enabled = False
|
||||
self.lru = []
|
||||
self.version = ((42 + 420 + 1984 + 1990 + 10 + 6 + 71 + 3004 + 7245) ^ 1337) + 4
|
||||
|
||||
async def get(self, args):
|
||||
if not self.enabled:
|
||||
return None
|
||||
await self.update_stat(args, "get")
|
||||
try:
|
||||
self.lru.pop(self.lru.index(args))
|
||||
except:
|
||||
# print("Cache miss!", args, flush=True)
|
||||
return None
|
||||
self.lru.insert(0, args)
|
||||
while len(self.lru) > self.max_items:
|
||||
self.cache.pop(self.lru[-1])
|
||||
self.lru.pop()
|
||||
# print("Cache hit!", args, flush=True)
|
||||
return self.cache[args]
|
||||
|
||||
async def get_stats(self):
|
||||
all_ = []
|
||||
for key in self.lru:
|
||||
all_.append(
|
||||
{
|
||||
"key": key,
|
||||
"set": self.stats[key]["set"],
|
||||
"get": self.stats[key]["get"],
|
||||
"delete": self.stats[key]["delete"],
|
||||
"value": str(self.serialize(self.cache[key].record)),
|
||||
}
|
||||
)
|
||||
return all_
|
||||
|
||||
def serialize(self, obj):
|
||||
cpy = obj.copy()
|
||||
cpy.pop("created_at", None)
|
||||
cpy.pop("deleted_at", None)
|
||||
cpy.pop("email", None)
|
||||
cpy.pop("password", None)
|
||||
return cpy
|
||||
|
||||
async def update_stat(self, key, action):
|
||||
if key not in self.stats:
|
||||
self.stats[key] = {"set": 0, "get": 0, "delete": 0}
|
||||
self.stats[key][action] = self.stats[key][action] + 1
|
||||
|
||||
def json_default(self, value):
|
||||
# if hasattr(value, "to_json"):
|
||||
# return value.to_json()
|
||||
try:
|
||||
return json.dumps(value.__dict__, default=str)
|
||||
except:
|
||||
return str(value)
|
||||
|
||||
async def create_cache_key(self, args, kwargs):
|
||||
return await security.hash(
|
||||
json.dumps(
|
||||
{"args": args, "kwargs": kwargs},
|
||||
sort_keys=True,
|
||||
default=self.json_default,
|
||||
)
|
||||
)
|
||||
|
||||
async def set(self, args, result):
|
||||
if not self.enabled:
|
||||
return
|
||||
is_new = args not in self.cache
|
||||
self.cache[args] = result
|
||||
await self.update_stat(args, "set")
|
||||
try:
|
||||
self.lru.pop(self.lru.index(args))
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
self.lru.insert(0, args)
|
||||
|
||||
while len(self.lru) > self.max_items:
|
||||
self.cache.pop(self.lru[-1])
|
||||
self.lru.pop()
|
||||
|
||||
if is_new:
|
||||
self.version += 1
|
||||
# print(f"Cache store! {len(self.lru)} items. New version:", self.version, flush=True)
|
||||
|
||||
async def delete(self, args):
|
||||
if not self.enabled:
|
||||
return
|
||||
await self.update_stat(args, "delete")
|
||||
if args in self.cache:
|
||||
try:
|
||||
self.lru.pop(self.lru.index(args))
|
||||
except IndexError:
|
||||
pass
|
||||
del self.cache[args]
|
||||
|
||||
def async_cache(self, func):
|
||||
@functools.wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
cache_key = await self.create_cache_key(args, kwargs)
|
||||
cached = await self.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
result = await func(*args, **kwargs)
|
||||
await self.set(cache_key, result)
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
def async_delete_cache(self, func):
|
||||
@functools.wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
cache_key = await self.create_cache_key(args, kwargs)
|
||||
if cache_key in self.cache:
|
||||
try:
|
||||
self.lru.pop(self.lru.index(cache_key))
|
||||
except IndexError:
|
||||
pass
|
||||
del self.cache[cache_key]
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def async_cache(func):
|
||||
B={}
|
||||
@functools.wraps(func)
|
||||
async def A(*A):
|
||||
if A in B:return B[A]
|
||||
C=await func(*A);B[A]=C;return C
|
||||
return A
|
||||
cache = {}
|
||||
|
||||
@functools.wraps(func)
|
||||
async def wrapper(*args):
|
||||
if args in cache:
|
||||
return cache[args]
|
||||
result = await func(*args)
|
||||
cache[args] = result
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
+115
-27
@@ -1,32 +1,120 @@
|
||||
_B='fields'
|
||||
_A=None
|
||||
# Written by retoor@molodetz.nl
|
||||
|
||||
# This code defines a framework for handling HTML elements as Python objects, including specific classes for HTML, form input, and form button elements. It offers methods to convert these elements to JSON, manipulate them, and validate form data.
|
||||
|
||||
# This code uses the `snek.system.model` library for managing model fields.
|
||||
|
||||
# MIT License
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from snek.system import model
|
||||
|
||||
|
||||
class HTMLElement(model.ModelField):
|
||||
def __init__(A,id=_A,tag='div',name=_A,html=_A,class_name=_A,text=_A,*B,**C):A.tag=tag;A.text=text;A.id=id;A.class_name=class_name or name;A.html=html;super().__init__(*B,name=name,**C)
|
||||
async def to_json(B):A=await super().to_json();A['text']=B.text;A['id']=B.id;A['html']=B.html;A['class_name']=B.class_name;A['tag']=B.tag;return A
|
||||
class FormElement(HTMLElement):0
|
||||
def __init__(
|
||||
self,
|
||||
id=None,
|
||||
tag="div",
|
||||
name=None,
|
||||
html=None,
|
||||
class_name=None,
|
||||
text=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.tag = tag
|
||||
self.text = text
|
||||
self.id = id
|
||||
self.class_name = class_name or name
|
||||
self.html = html
|
||||
super().__init__(name=name, *args, **kwargs)
|
||||
|
||||
async def to_json(self):
|
||||
result = await super().to_json()
|
||||
result["text"] = self.text
|
||||
result["id"] = self.id
|
||||
result["html"] = self.html
|
||||
result["class_name"] = self.class_name
|
||||
result["tag"] = self.tag
|
||||
return result
|
||||
|
||||
|
||||
class FormElement(HTMLElement):
|
||||
pass
|
||||
|
||||
|
||||
class FormInputElement(FormElement):
|
||||
def __init__(A,type='text',place_holder=_A,*B,**C):super().__init__(*B,tag='input',**C);A.place_holder=place_holder;A.type=type
|
||||
async def to_json(B):A=await super().to_json();A['place_holder']=B.place_holder;A['type']=B.type;return A
|
||||
def __init__(self, type="text", place_holder=None, *args, **kwargs):
|
||||
super().__init__(tag="input", *args, **kwargs)
|
||||
self.place_holder = place_holder
|
||||
self.type = type
|
||||
|
||||
async def to_json(self):
|
||||
data = await super().to_json()
|
||||
data["place_holder"] = self.place_holder
|
||||
data["type"] = self.type
|
||||
return data
|
||||
|
||||
|
||||
class FormButtonElement(FormElement):
|
||||
def __init__(C,tag='button',*A,**B):super().__init__(*A,tag=tag,**B)
|
||||
def __init__(self, tag="button", *args, **kwargs):
|
||||
super().__init__(tag=tag, *args, **kwargs)
|
||||
|
||||
|
||||
class Form(model.BaseModel):
|
||||
@property
|
||||
def html_elements(self):return[A for A in self.fields if isinstance(A,HTMLElement)]
|
||||
def set_user_data(A,data):return super().set_user_data(data.get(_B))
|
||||
async def to_json(D,encode=False):
|
||||
B='is_valid';E=await super().to_json();C={}
|
||||
for A in E.keys():
|
||||
if A==B:continue
|
||||
F=getattr(D,A)
|
||||
if isinstance(F,HTMLElement):
|
||||
try:C[A]=E[A]
|
||||
except KeyError:pass
|
||||
G=all(A[B]for A in C.values());return{_B:C,B:G,'errors':await D.errors}
|
||||
@property
|
||||
async def errors(self):
|
||||
A=[]
|
||||
for B in self.html_elements:A+=await B.errors
|
||||
return A
|
||||
@property
|
||||
async def is_valid(self):return False
|
||||
@property
|
||||
def html_elements(self):
|
||||
return [element for element in self.fields if isinstance(element, HTMLElement)]
|
||||
|
||||
def set_user_data(self, data):
|
||||
return super().set_user_data(data.get("fields"))
|
||||
|
||||
async def to_json(self, encode=False):
|
||||
elements = await super().to_json()
|
||||
html_elements = {}
|
||||
for element in elements.keys():
|
||||
if element == "is_valid":
|
||||
# is_valid is async get property so we can't do getattr on it
|
||||
continue
|
||||
field = getattr(self, element)
|
||||
if isinstance(field, HTMLElement):
|
||||
try:
|
||||
html_elements[element] = elements[element]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
is_valid = all(field["is_valid"] for field in html_elements.values())
|
||||
return {
|
||||
"fields": html_elements,
|
||||
"is_valid": is_valid,
|
||||
"errors": await self.errors,
|
||||
}
|
||||
|
||||
@property
|
||||
async def errors(self):
|
||||
result = []
|
||||
for field in self.html_elements:
|
||||
result += await field.errors
|
||||
return result
|
||||
|
||||
@property
|
||||
async def is_valid(self):
|
||||
# This is not good, but timebox to resolve issue exceeded.
|
||||
return False
|
||||
|
||||
+103
-37
@@ -1,44 +1,110 @@
|
||||
import asyncio,pathlib,uuid,zlib
|
||||
# Written by retoor@molodetz.nl
|
||||
|
||||
# This script enables downloading, processing, and caching web content, including taking website screenshots and repairing links in HTML content.
|
||||
|
||||
# Imports used: aiohttp, aiohttp.web for creating web servers and handling async requests; app.cache for caching utilities; BeautifulSoup from bs4 for HTML parsing; imgkit for creating screenshots.
|
||||
|
||||
# The MIT License (MIT)
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
|
||||
import asyncio
|
||||
import pathlib
|
||||
import uuid
|
||||
import zlib
|
||||
from urllib.parse import urljoin
|
||||
import aiohttp,imgkit
|
||||
|
||||
import aiohttp
|
||||
import imgkit
|
||||
from app.cache import time_cache_async
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
async def crc32(data):
|
||||
A=data
|
||||
try:A=A.encode()
|
||||
except:pass
|
||||
return'crc32'+str(zlib.crc32(A))
|
||||
async def get_file(name,suffix='.cache'):
|
||||
A=name;A=await crc32(A);B=pathlib.Path('.').joinpath('cache')
|
||||
if not B.exists():B.mkdir(parents=True,exist_ok=True)
|
||||
return B.joinpath(A+suffix)
|
||||
async def public_touch(name=None):A=pathlib.Path('.').joinpath(str(uuid.uuid4())+name);A.open('wb').close();return A
|
||||
try:
|
||||
data = data.encode()
|
||||
except:
|
||||
pass
|
||||
return "crc32" + str(zlib.crc32(data))
|
||||
|
||||
|
||||
async def get_file(name, suffix=".cache"):
|
||||
name = await crc32(name)
|
||||
path = pathlib.Path(".").joinpath("cache")
|
||||
if not path.exists():
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path.joinpath(name + suffix)
|
||||
|
||||
|
||||
async def public_touch(name=None):
|
||||
path = pathlib.Path(".").joinpath(str(uuid.uuid4()) + name)
|
||||
path.open("wb").close()
|
||||
return path
|
||||
|
||||
|
||||
async def create_site_photo(url):
|
||||
A=url;C=asyncio.get_event_loop()
|
||||
if not A.startswith('https'):A='https://'+A
|
||||
B=await get_file('site-screenshot-'+A,'.png')
|
||||
if B.exists():return B
|
||||
B.touch()
|
||||
def D():imgkit.from_url(A,B.absolute());return B
|
||||
return await C.run_in_executor(None,D)
|
||||
async def repair_links(base_url,html_content):
|
||||
D='http';E=base_url;B='src';C='href';F=BeautifulSoup(html_content,'html.parser')
|
||||
for A in F.find_all(['a','img','link']):
|
||||
if A.has_attr(C)and not A[C].startswith(D):A[C]=urljoin(E,A[C])
|
||||
if A.has_attr(B)and not A[B].startswith(D):A[B]=urljoin(E,A[B])
|
||||
return F.prettify()
|
||||
async def is_html_content(content):
|
||||
B=False;A=content
|
||||
if not A:return B
|
||||
try:A=A.decode(errors='ignore')
|
||||
except:pass
|
||||
C=['<html','<img','<p','<span','<div'];A=A.lower()
|
||||
for D in C:
|
||||
if D in A:return True
|
||||
return B
|
||||
loop = asyncio.get_event_loop()
|
||||
if not url.startswith("https"):
|
||||
url = "https://" + url
|
||||
output_path = await get_file("site-screenshot-" + url, ".png")
|
||||
|
||||
if output_path.exists():
|
||||
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):
|
||||
soup = BeautifulSoup(html_content, "html.parser")
|
||||
for tag in soup.find_all(["a", "img", "link"]):
|
||||
if tag.has_attr("href") and not tag["href"].startswith("http"):
|
||||
tag["href"] = urljoin(base_url, tag["href"])
|
||||
if tag.has_attr("src") and not tag["src"].startswith("http"):
|
||||
tag["src"] = urljoin(base_url, tag["src"])
|
||||
return soup.prettify()
|
||||
|
||||
|
||||
async def is_html_content(content: bytes):
|
||||
if not content:
|
||||
return False
|
||||
try:
|
||||
content = content.decode(errors="ignore")
|
||||
except:
|
||||
pass
|
||||
marks = ["<html", "<img", "<p", "<span", "<div"]
|
||||
content = content.lower()
|
||||
for mark in marks:
|
||||
if mark in content:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@time_cache_async(120)
|
||||
async def get(url):
|
||||
async with aiohttp.ClientSession()as B:
|
||||
C=await B.get(url);A=await C.text()
|
||||
if await is_html_content(A):A=(await repair_links(url,A)).encode()
|
||||
return A
|
||||
async with aiohttp.ClientSession() as session:
|
||||
response = await session.get(url)
|
||||
content = await response.text()
|
||||
if await is_html_content(content):
|
||||
content = (await repair_links(url, content)).encode()
|
||||
return content
|
||||
|
||||
+75
-34
@@ -1,37 +1,78 @@
|
||||
_A='uid'
|
||||
DEFAULT_LIMIT=30
|
||||
DEFAULT_LIMIT = 30
|
||||
import typing
|
||||
import asyncio
|
||||
from snek.system.model import BaseModel
|
||||
|
||||
|
||||
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
|
||||
@property
|
||||
def db(self):return self.app.db
|
||||
async def new(A):return A.model_class(mapper=A,app=A.app)
|
||||
@property
|
||||
def table(self):return self.db[self.table_name]
|
||||
async def get(B,uid=None,**C):
|
||||
if uid:C[_A]=uid
|
||||
A=B.table.find_one(**C)
|
||||
if not A:return
|
||||
A=dict(A);D=await B.new()
|
||||
for(E,F)in A.items():D[E]=F
|
||||
return D;return await B.model_class.from_record(mapper=B,record=A)
|
||||
async def exists(A,**B):return A.table.exists(**B)
|
||||
async def count(A,**B):return A.table.count(**B)
|
||||
async def save(B,model):
|
||||
A=model
|
||||
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 find(A,**B):
|
||||
C='_limit'
|
||||
if not B.get(C):B[C]=A.default_limit
|
||||
for E in A.table.find(**B):
|
||||
D=await A.new()
|
||||
for(F,G)in E.items():D[F]=G
|
||||
yield D
|
||||
async def query(A,sql,*B):
|
||||
for C in A.db.query(sql,*B):yield dict(C)
|
||||
async def delete(B,**A):
|
||||
if not A or not isinstance(A,dict):raise Exception("Can't execute delete with no filter.")
|
||||
return B.table.delete(**A)
|
||||
|
||||
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
|
||||
def db(self):
|
||||
return self.app.db
|
||||
|
||||
@property
|
||||
def loop(self):
|
||||
return asyncio.get_event_loop()
|
||||
|
||||
async def run_in_executor(self, func, *args, **kwargs):
|
||||
return await self.loop.run_in_executor(None, lambda: func(*args, **kwargs))
|
||||
|
||||
async def new(self):
|
||||
return self.model_class(mapper=self, app=self.app)
|
||||
|
||||
@property
|
||||
def table(self):
|
||||
return self.db[self.table_name]
|
||||
|
||||
async def get(self, uid: str = None, **kwargs) -> BaseModel:
|
||||
if uid:
|
||||
kwargs["uid"] = uid
|
||||
|
||||
record = await self.run_in_executor(self.table.find_one,**kwargs)
|
||||
if not record:
|
||||
return None
|
||||
record = dict(record)
|
||||
model = await self.new()
|
||||
for key, value in record.items():
|
||||
model[key] = value
|
||||
return model
|
||||
return await self.model_class.from_record(mapper=self, record=record)
|
||||
|
||||
async def exists(self, **kwargs):
|
||||
return await self.run_in_executor(self.table.exists,**kwargs)
|
||||
|
||||
async def count(self, **kwargs) -> int:
|
||||
return await self.run_in_executor(self.table.count, **kwargs)
|
||||
|
||||
async def save(self, model: BaseModel) -> bool:
|
||||
if not model.record.get("uid"):
|
||||
raise Exception(f"Attempt to save without uid: {model.record}.")
|
||||
model.updated_at.update()
|
||||
return await self.run_in_executor(self.table.upsert, model.record, ["uid"])
|
||||
|
||||
async def find(self, **kwargs) -> typing.AsyncGenerator:
|
||||
if not kwargs.get("_limit"):
|
||||
kwargs["_limit"] = self.default_limit
|
||||
for record in await self.run_in_executor(self.table.find, **kwargs):
|
||||
model = await self.new()
|
||||
for key, value in record.items():
|
||||
model[key] = value
|
||||
yield model
|
||||
|
||||
async def query(self, sql, *args):
|
||||
for record in await self.run_in_executor(self.db.query,sql, *args):
|
||||
yield dict(record)
|
||||
|
||||
async def delete(self, **kwargs) -> int:
|
||||
if not kwargs or not isinstance(kwargs, dict):
|
||||
raise Exception("Can't execute delete with no filter.")
|
||||
return await self.run_in_executor(self.table.delete, **kwargs)
|
||||
|
||||
+90
-25
@@ -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 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.formatters import html
|
||||
from pygments.lexers import get_lexer_by_name
|
||||
|
||||
|
||||
class MarkdownRenderer(HTMLRenderer):
|
||||
_allow_harmful_protocols=_A
|
||||
def __init__(A,app,template):A.template=template;A.app=app;A.env=A.app.jinja2_env;B=html.HtmlFormatter();A.env.globals['highlight_styles']=B.get_style_defs()
|
||||
def _escape(A,str):return str
|
||||
def get_lexer(A,lang,default='bash'):
|
||||
try:return get_lexer_by_name(lang,stripall=_A)
|
||||
except:return get_lexer_by_name(default,stripall=_A)
|
||||
def block_code(B,code,lang=None,info=None):
|
||||
A=lang
|
||||
if not A:A=info
|
||||
if not A:A='bash'
|
||||
C=B.get_lexer(A);D=html.HtmlFormatter(lineseparator='<br>');E=highlight(code,C,D);return E
|
||||
def render(A):B=A.app.template_path.joinpath(A.template).read_text();C=MarkdownRenderer(A.app,A.template);D=Markdown(renderer=C);return D(B)
|
||||
def render_markdown_sync(app,markdown_string):A=MarkdownRenderer(app,None);B=Markdown(renderer=A);return B(markdown_string)
|
||||
|
||||
_allow_harmful_protocols = True
|
||||
|
||||
def __init__(self, app, template):
|
||||
super().__init__(False, True)
|
||||
|
||||
self.template = template
|
||||
|
||||
self.app = app
|
||||
self.env = self.app.jinja2_env
|
||||
formatter = html.HtmlFormatter()
|
||||
self.env.globals["highlight_styles"] = formatter.get_style_defs()
|
||||
|
||||
def _escape(self, str):
|
||||
return str ##escape(str)
|
||||
|
||||
def get_lexer(self, lang, default="bash"):
|
||||
try:
|
||||
return get_lexer_by_name(lang, stripall=True)
|
||||
except:
|
||||
return get_lexer_by_name(default, stripall=True)
|
||||
|
||||
def block_code(self, code, lang=None, info=None):
|
||||
if not lang:
|
||||
lang = info
|
||||
if not lang:
|
||||
lang = "bash"
|
||||
lexer = self.get_lexer(lang)
|
||||
formatter = html.HtmlFormatter(lineseparator="<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)
|
||||
async def render_markdown(app,markdown_string):return render_markdown_sync(app,markdown_string)
|
||||
from jinja2 import TemplateSyntaxError,nodes
|
||||
async def render_markdown(app, markdown_string):
|
||||
return render_markdown_sync(app, markdown_string)
|
||||
|
||||
|
||||
from jinja2 import TemplateSyntaxError, nodes
|
||||
from jinja2.ext import Extension
|
||||
from jinja2.nodes import Const
|
||||
|
||||
|
||||
# Source: https://ron.sh/how-to-write-a-jinja2-extension/
|
||||
class MarkdownExtension(Extension):
|
||||
tags={'markdown'}
|
||||
def __init__(A,environment):B=environment;A.app=SimpleNamespace(jinja2_env=B);super(MarkdownExtension,A).__init__(B)
|
||||
def parse(D,parser):
|
||||
A=parser;E=next(A.stream).lineno;B=[Const('')];C=''
|
||||
try:B=[A.parse_expression()]
|
||||
except TemplateSyntaxError:C=A.parse_statements(['name:endmarkdown'],drop_needle=_A)
|
||||
return nodes.CallBlock(D.call_method('_to_html',B),[],[],C).set_lineno(E)
|
||||
def _to_html(A,md_file,caller):return render_markdown_sync(A.app,caller())
|
||||
tags = {"markdown"}
|
||||
|
||||
def __init__(self, environment):
|
||||
self.app = SimpleNamespace(jinja2_env=environment)
|
||||
super(MarkdownExtension, self).__init__(environment)
|
||||
|
||||
def parse(self, parser):
|
||||
line_number = next(parser.stream).lineno
|
||||
md_file = [Const("")]
|
||||
body = ""
|
||||
try:
|
||||
md_file = [parser.parse_expression()]
|
||||
except TemplateSyntaxError:
|
||||
body = parser.parse_statements(["name:endmarkdown"], drop_needle=True)
|
||||
return nodes.CallBlock(
|
||||
self.call_method("_to_html", md_file), [], [], body
|
||||
).set_lineno(line_number)
|
||||
|
||||
def _to_html(self, md_file, caller):
|
||||
return render_markdown_sync(self.app, caller())
|
||||
|
||||
@@ -1,21 +1,53 @@
|
||||
_D='Access-Control-Allow-Credentials'
|
||||
_C='Access-Control-Allow-Headers'
|
||||
_B='Access-Control-Allow-Methods'
|
||||
_A='Access-Control-Allow-Origin'
|
||||
# Written by retoor@molodetz.nl
|
||||
|
||||
# This code provides middleware functions for an aiohttp server to manage and modify CORS (Cross-Origin Resource Sharing) headers.
|
||||
|
||||
# Imports from 'aiohttp' library are used to create middleware; they are not part of Python's standard library.
|
||||
|
||||
# MIT License: This code is distributed under the MIT License.
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def no_cors_middleware(request,handler):A=await handler(request);A.headers.pop(_A,None);return A
|
||||
async def no_cors_middleware(request, handler):
|
||||
response = await handler(request)
|
||||
response.headers.pop("Access-Control-Allow-Origin", None)
|
||||
return response
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def cors_allow_middleware(request,handler):A=await handler(request);A.headers[_A]='*';A.headers[_B]='GET, POST, OPTIONS, PUT, DELETE, MOVE, COPY, HEAD, LOCK, UNLOCK, PATCH, PROPFIND';A.headers[_C]='*';A.headers[_D]='true';return A
|
||||
async def cors_allow_middleware(request, handler):
|
||||
response = await handler(request)
|
||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
response.headers["Access-Control-Allow-Methods"] = (
|
||||
"GET, POST, OPTIONS, PUT, DELETE, MOVE, COPY, HEAD, LOCK, UNLOCK, PATCH, PROPFIND"
|
||||
)
|
||||
response.headers["Access-Control-Allow-Headers"] = "*"
|
||||
response.headers["Access-Control-Allow-Credentials"] = "true"
|
||||
return response
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def auth_middleware(request,handler):
|
||||
B='uid';C='user';A=request;A[C]=None
|
||||
if A.session.get(B)and A.session.get('logged_in'):A[C]=await A.app.services.user.get(uid=A.app.session.get(B))
|
||||
return await handler(A)
|
||||
async def auth_middleware(request, handler):
|
||||
request["user"] = None
|
||||
if request.session.get("uid") and request.session.get("logged_in"):
|
||||
request["user"] = await request.app.services.user.get(
|
||||
uid=request.app.session.get("uid")
|
||||
)
|
||||
return await handler(request)
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def cors_middleware(request,handler):
|
||||
C='Allow';D=handler;B=request
|
||||
if B.headers.get(C):return await D(B)
|
||||
A=await D(B)
|
||||
if B.headers.get(C):return A
|
||||
A.headers[_A]='*';A.headers[_B]='GET, POST, PUT, DELETE, OPTIONS';A.headers[_C]='*';A.headers[_D]='true';return A
|
||||
async def cors_middleware(request, handler):
|
||||
if request.headers.get("Allow"):
|
||||
return await handler(request)
|
||||
|
||||
response = await handler(request)
|
||||
if request.headers.get("Allow"):
|
||||
return response
|
||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
|
||||
response.headers["Access-Control-Allow-Headers"] = "*"
|
||||
response.headers["Access-Control-Allow-Credentials"] = "true"
|
||||
return response
|
||||
|
||||
+367
-129
@@ -1,139 +1,377 @@
|
||||
_I='deleted_at'
|
||||
_H='updated_at'
|
||||
_G='created_at'
|
||||
_F='is_valid'
|
||||
_E='name'
|
||||
_D=False
|
||||
_C='value'
|
||||
_B=True
|
||||
_A=None
|
||||
import copy,json,re,uuid
|
||||
# Written by retoor@molodetz.nl
|
||||
|
||||
# The script defines a flexible validation and field management system for models, with capabilities for setting attributes, validation, error handling, and JSON conversion. It includes classes for managing various field types with specific properties such as UUID, timestamps for creation and updates, and custom validation rules.
|
||||
|
||||
# This script utilizes external Python libraries such as 're' for regex operations, 'uuid' for generating unique identifiers, and 'json' for data interchange. The 'datetime' and 'timezone' modules from the Python standard library are used for date and time operations. 'OrderedDict' from 'collections' provides enhanced dictionary capabilities, and 'copy' allows deep copying of objects.
|
||||
|
||||
# MIT License
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime,timezone
|
||||
TIMESTAMP_REGEX='^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}\\+\\d{2}:\\d{2}$'
|
||||
def now():return str(datetime.now(timezone.utc))
|
||||
def add_attrs(**A):
|
||||
def B(func):
|
||||
for(B,C)in A.items():setattr(func,B,C)
|
||||
return func
|
||||
return B
|
||||
def validate_attrs(required=_D,min_length=_A,max_length=_A,regex=_A,**A):
|
||||
def B(func):return add_attrs(required=required,min_length=min_length,max_length=max_length,regex=regex,**A)(func)
|
||||
from datetime import datetime, timezone
|
||||
|
||||
TIMESTAMP_REGEX = r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}\+\d{2}:\d{2}$"
|
||||
|
||||
|
||||
def now():
|
||||
return str(datetime.now(timezone.utc))
|
||||
|
||||
|
||||
def add_attrs(**kwargs):
|
||||
def decorator(func):
|
||||
for key, value in kwargs.items():
|
||||
setattr(func, key, value)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def validate_attrs(
|
||||
required=False, min_length=None, max_length=None, regex=None, **kwargs
|
||||
):
|
||||
def decorator(func):
|
||||
return add_attrs(
|
||||
required=required,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
regex=regex,
|
||||
**kwargs,
|
||||
)(func)
|
||||
|
||||
|
||||
class Validator:
|
||||
_index=0
|
||||
@property
|
||||
def value(self):return self._value
|
||||
@value.setter
|
||||
def value(self,val):self._value=json.loads(json.dumps(val,default=str))
|
||||
@property
|
||||
def initial_value(self):return self.value
|
||||
def custom_validation(A):return _B
|
||||
def __init__(A,required=_D,min_num=_A,max_num=_A,min_length=_A,max_length=_A,regex=_A,value=_A,kind=_A,help_text=_A,app=_A,model=_A,**B):A.index=Validator._index;Validator._index+=1;A.app=app;A.model=model;A.required=required;A.min_num=min_num;A.max_num=max_num;A.min_length=min_length;A.max_length=max_length;A.regex=regex;A._value=_A;A.value=value;A.kind=kind;A.help_text=help_text;A.__dict__.update(B)
|
||||
@property
|
||||
async def errors(self):
|
||||
A=self;B=[]
|
||||
if A.value is _A and A.required:B.append('Field is required.');return B
|
||||
if A.value is _A:return B
|
||||
if A.kind in[int,float]:
|
||||
if A.min_num is not _A and A.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 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.")
|
||||
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 A.kind and not isinstance(A.value,A.kind):B.append(f"Invalid kind. It is supposed to be {A.kind}.")
|
||||
return B
|
||||
async def validate(B):
|
||||
A=await B.errors
|
||||
if A:raise ValueError(f"Errors: {A}.")
|
||||
return _B
|
||||
def __repr__(A):return str(A.to_json())
|
||||
@property
|
||||
async def is_valid(self):
|
||||
try:await self.validate();return _B
|
||||
except ValueError:return _D
|
||||
async def to_json(A):B=await A.errors;C=await A.is_valid;return{'required':A.required,'min_num':A.min_num,'max_num':A.max_num,'min_length':A.min_length,'max_length':A.max_length,'regex':A.regex,_C:A.value,'kind':str(A.kind),'help_text':A.help_text,'errors':B,_F:C,'index':A.index}
|
||||
_index = 0
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, val):
|
||||
self._value = json.loads(json.dumps(val, default=str))
|
||||
|
||||
@property
|
||||
def initial_value(self):
|
||||
return self.value
|
||||
|
||||
def custom_validation(self):
|
||||
return True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
required=False,
|
||||
min_num=None,
|
||||
max_num=None,
|
||||
min_length=None,
|
||||
max_length=None,
|
||||
regex=None,
|
||||
value=None,
|
||||
kind=None,
|
||||
help_text=None,
|
||||
app=None,
|
||||
model=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.index = Validator._index
|
||||
Validator._index += 1
|
||||
self.app = app
|
||||
self.model = model
|
||||
self.required = required
|
||||
self.min_num = min_num
|
||||
self.max_num = max_num
|
||||
self.min_length = min_length
|
||||
self.max_length = max_length
|
||||
self.regex = regex
|
||||
self._value = None
|
||||
self.value = value
|
||||
self.kind = kind
|
||||
self.help_text = help_text
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
@property
|
||||
async def errors(self):
|
||||
error_list = []
|
||||
if self.value is None and self.required:
|
||||
error_list.append("Field is required.")
|
||||
return error_list
|
||||
|
||||
if self.value is None:
|
||||
return error_list
|
||||
|
||||
if self.kind in [int, float]:
|
||||
if self.min_num is not None and self.value < self.min_num:
|
||||
error_list.append(f"Field should be minimal {self.min_num}.")
|
||||
if self.max_num is not None and self.value > self.max_num:
|
||||
error_list.append(f"Field should be maximal {self.max_num}.")
|
||||
if self.min_length is not None and len(self.value) < self.min_length:
|
||||
error_list.append(
|
||||
f"Field should be minimal {self.min_length} characters long."
|
||||
)
|
||||
if self.max_length is not None and len(self.value) > self.max_length:
|
||||
error_list.append(
|
||||
f"Field should be maximal {self.max_length} characters long."
|
||||
)
|
||||
if self.regex and self.value and not re.match(self.regex, self.value):
|
||||
error_list.append("Invalid value.")
|
||||
if self.kind and not isinstance(self.value, self.kind):
|
||||
error_list.append(f"Invalid kind. It is supposed to be {self.kind}.")
|
||||
return error_list
|
||||
|
||||
async def validate(self):
|
||||
errors = await self.errors
|
||||
if errors:
|
||||
raise ValueError(f"Errors: {errors}.")
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.to_json())
|
||||
|
||||
@property
|
||||
async def is_valid(self):
|
||||
try:
|
||||
await self.validate()
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
async def to_json(self):
|
||||
errors = await self.errors
|
||||
is_valid = await self.is_valid
|
||||
return {
|
||||
"required": self.required,
|
||||
"min_num": self.min_num,
|
||||
"max_num": self.max_num,
|
||||
"min_length": self.min_length,
|
||||
"max_length": self.max_length,
|
||||
"regex": self.regex,
|
||||
"value": self.value,
|
||||
"kind": str(self.kind),
|
||||
"help_text": self.help_text,
|
||||
"errors": errors,
|
||||
"is_valid": is_valid,
|
||||
"index": self.index,
|
||||
}
|
||||
|
||||
|
||||
class ModelField(Validator):
|
||||
index=1
|
||||
def __init__(A,name=_A,save=_B,*B,**C):A.name=name;A.save=save;super().__init__(*B,**C)
|
||||
async def to_json(B):A=await super().to_json();A[_E]=B.name;return A
|
||||
|
||||
index = 1
|
||||
|
||||
def __init__(self, name=None, save=True, *args, **kwargs):
|
||||
self.name = name
|
||||
self.save = save
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def to_json(self):
|
||||
result = await super().to_json()
|
||||
result["name"] = self.name
|
||||
return result
|
||||
|
||||
|
||||
class CreatedField(ModelField):
|
||||
@property
|
||||
def initial_value(self):return now()
|
||||
def update(A):
|
||||
if not A.value:A.value=now()
|
||||
|
||||
@property
|
||||
def initial_value(self):
|
||||
return now()
|
||||
|
||||
def update(self):
|
||||
if not self.value:
|
||||
self.value = now()
|
||||
|
||||
|
||||
class UpdatedField(ModelField):
|
||||
def update(A):A.value=now()
|
||||
|
||||
def update(self):
|
||||
self.value = now()
|
||||
|
||||
|
||||
class DeletedField(ModelField):
|
||||
def update(A):A.value=now()
|
||||
|
||||
def update(self):
|
||||
self.value = now()
|
||||
|
||||
|
||||
class UUIDField(ModelField):
|
||||
@property
|
||||
def value(self):return str(self._value)
|
||||
@value.setter
|
||||
def value(self,val):self._value=str(val)
|
||||
@property
|
||||
def initial_value(self):return str(uuid.uuid4())
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return str(self._value)
|
||||
|
||||
@value.setter
|
||||
def value(self, val):
|
||||
self._value = str(val)
|
||||
|
||||
@property
|
||||
def initial_value(self):
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
class BaseModel:
|
||||
uid=UUIDField(name='uid',required=_B);created_at=CreatedField(name=_G,required=_B,regex=TIMESTAMP_REGEX,place_holder='Created at');updated_at=UpdatedField(name=_H,regex=TIMESTAMP_REGEX,place_holder='Updated at');deleted_at=DeletedField(name=_I,regex=TIMESTAMP_REGEX,place_holder='Deleted at')
|
||||
@classmethod
|
||||
async def from_record(B,record,mapper):A=B();A.mapper=mapper;A.record=record;return A
|
||||
@property
|
||||
def mapper(self):return self._mapper
|
||||
@mapper.setter
|
||||
def mapper(self,value):self._mapper=value
|
||||
@property
|
||||
def record(self):return{A:B.value for(A,B)in self.fields.items()}
|
||||
@record.setter
|
||||
def record(self,val):
|
||||
A=self
|
||||
for(B,C)in val.items():
|
||||
D=A.fields.get(B)
|
||||
if not D:continue
|
||||
A[B]=C
|
||||
return A
|
||||
def __init__(A,*F,**C):
|
||||
D='app';A._mapper=C.get('mapper');A.app=C.get(D);A.fields={}
|
||||
for B in dir(A.__class__):
|
||||
E=getattr(A.__class__,B)
|
||||
if isinstance(E,Validator):A.__dict__[B]=copy.deepcopy(E);A.__dict__[B].value=C.pop(B,A.__dict__[B].initial_value);A.fields[B]=A.__dict__[B];A.fields[B].model=A;A.fields[B].app=C.get(D)
|
||||
def __setitem__(B,key,value):
|
||||
A=B.__dict__.get(key)
|
||||
if isinstance(A,Validator):A.value=value
|
||||
def __getattr__(B,key):
|
||||
A=B.__dict__.get(key)
|
||||
if isinstance(A,Validator):return A.value
|
||||
return A
|
||||
def set_user_data(C,data):
|
||||
for(D,A)in data.items():
|
||||
B=C.fields.get(D)
|
||||
if not B:continue
|
||||
if A.get(_E):A=A.get(_C)
|
||||
B.value=A
|
||||
@property
|
||||
async def is_valid(self):return all([await A.is_valid for A in self.fields.values()])
|
||||
def __getitem__(B,key):
|
||||
A=B.__dict__.get(key)
|
||||
if isinstance(A,Validator):return A.value
|
||||
def __setattr__(A,key,value):
|
||||
B=value;C=getattr(A,key)
|
||||
if isinstance(C,Validator):C.value=B
|
||||
else:A.__dict__[key]=B
|
||||
@property
|
||||
async def recordz(self):
|
||||
D=await self.to_json();B={}
|
||||
for(C,A)in D.items():
|
||||
if not isinstance(A,dict)or _C not in A:continue
|
||||
if getattr(self,C).save:B[C]=A.get(_C)
|
||||
return B
|
||||
async def to_json(A,encode=_D):
|
||||
B=OrderedDict({'uid':A.uid.value,_G:A.created_at.value,_H:A.updated_at.value,_I:A.deleted_at.value,_F:await A.is_valid})
|
||||
for(C,D)in A.fields.items():
|
||||
if C=='record':continue
|
||||
D=A.__dict__[C]
|
||||
if hasattr(D,_C):B[C]=await D.to_json()
|
||||
if encode:return json.dumps(B,indent=2)
|
||||
return B
|
||||
|
||||
uid = UUIDField(name="uid", required=True)
|
||||
created_at = CreatedField(
|
||||
name="created_at",
|
||||
required=True,
|
||||
regex=TIMESTAMP_REGEX,
|
||||
place_holder="Created at",
|
||||
)
|
||||
updated_at = UpdatedField(
|
||||
name="updated_at", regex=TIMESTAMP_REGEX, place_holder="Updated at"
|
||||
)
|
||||
deleted_at = DeletedField(
|
||||
name="deleted_at", regex=TIMESTAMP_REGEX, place_holder="Deleted at"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def from_record(cls, record, mapper):
|
||||
model = cls()
|
||||
model.mapper = mapper
|
||||
model.record = record
|
||||
return model
|
||||
|
||||
@property
|
||||
def mapper(self):
|
||||
return self._mapper
|
||||
|
||||
@mapper.setter
|
||||
def mapper(self, value):
|
||||
self._mapper = value
|
||||
|
||||
@property
|
||||
def record(self):
|
||||
return {key: field.value for key, field in self.fields.items()}
|
||||
|
||||
@record.setter
|
||||
def record(self, val):
|
||||
for key, value in val.items():
|
||||
field = self.fields.get(key)
|
||||
if not field:
|
||||
continue
|
||||
self[key] = value
|
||||
return self
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._mapper = kwargs.get("mapper")
|
||||
self.app = kwargs.get("app")
|
||||
self.fields = {}
|
||||
for key in dir(self.__class__):
|
||||
obj = getattr(self.__class__, key)
|
||||
|
||||
if isinstance(obj, Validator):
|
||||
self.__dict__[key] = copy.deepcopy(obj)
|
||||
self.__dict__[key].value = kwargs.pop(
|
||||
key, self.__dict__[key].initial_value
|
||||
)
|
||||
self.fields[key] = self.__dict__[key]
|
||||
self.fields[key].model = self
|
||||
self.fields[key].app = kwargs.get("app")
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
obj = self.__dict__.get(key)
|
||||
if isinstance(obj, Validator):
|
||||
obj.value = value
|
||||
|
||||
def __getattr__(self, key):
|
||||
obj = self.__dict__.get(key)
|
||||
if isinstance(obj, Validator):
|
||||
return obj.value
|
||||
return obj
|
||||
|
||||
def set_user_data(self, data):
|
||||
for key, value in data.items():
|
||||
field = self.fields.get(key)
|
||||
if not field:
|
||||
continue
|
||||
if value.get("name"):
|
||||
value = value.get("value")
|
||||
field.value = value
|
||||
|
||||
@property
|
||||
async def is_valid(self):
|
||||
return all([await field.is_valid for field in self.fields.values()])
|
||||
|
||||
def __getitem__(self, key):
|
||||
obj = self.__dict__.get(key)
|
||||
if isinstance(obj, Validator):
|
||||
return obj.value
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
obj = getattr(self, key)
|
||||
if isinstance(obj, Validator):
|
||||
obj.value = value
|
||||
else:
|
||||
self.__dict__[key] = value
|
||||
|
||||
@property
|
||||
async def recordz(self):
|
||||
obj = await self.to_json()
|
||||
record = {}
|
||||
for key, value in obj.items():
|
||||
if not isinstance(value, dict) or "value" not in value:
|
||||
continue
|
||||
if getattr(self, key).save:
|
||||
record[key] = value.get("value")
|
||||
return record
|
||||
|
||||
async def to_json(self, encode=False):
|
||||
model_data = OrderedDict(
|
||||
{
|
||||
"uid": self.uid.value,
|
||||
"created_at": self.created_at.value,
|
||||
"updated_at": self.updated_at.value,
|
||||
"deleted_at": self.deleted_at.value,
|
||||
"is_valid": await self.is_valid,
|
||||
}
|
||||
)
|
||||
|
||||
for key, value in self.fields.items():
|
||||
if key == "record":
|
||||
continue
|
||||
value = self.__dict__[key]
|
||||
if hasattr(value, "value"):
|
||||
model_data[key] = await value.to_json()
|
||||
if encode:
|
||||
return json.dumps(model_data, indent=2)
|
||||
return model_data
|
||||
|
||||
|
||||
class FormElement(ModelField):
|
||||
def __init__(A,place_holder=_A,*B,**C):super().__init__(*B,**C);A.place_holder=place_holder
|
||||
|
||||
def __init__(self, place_holder=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.place_holder = place_holder
|
||||
|
||||
|
||||
class FormElement(ModelField):
|
||||
def __init__(A,place_holder=_A,*B,**C):A.place_holder=place_holder;super().__init__(*B,**C)
|
||||
async def to_json(B):A=await super().to_json();A[_E]=B.name;A['place_holder']=B.place_holder;return A
|
||||
|
||||
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:
|
||||
def __init__(A,*C,**D):
|
||||
for B in C:
|
||||
if isinstance(B,dict):A.__dict__.update(B)
|
||||
A.__dict__.update(D)
|
||||
def __getitem__(A,key):return A.__dict__[key]
|
||||
def __setitem__(A,key,value):A.__dict__[key]=value
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
for arg in args:
|
||||
if isinstance(arg, dict):
|
||||
self.__dict__.update(arg)
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.__dict__[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.__dict__[key] = value
|
||||
|
||||
+42
-13
@@ -1,17 +1,46 @@
|
||||
import cProfile,pstats,sys
|
||||
import cProfile
|
||||
import pstats
|
||||
import sys
|
||||
|
||||
from aiohttp import web
|
||||
profiler=None
|
||||
|
||||
profiler = None
|
||||
import io
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def profile_middleware(request,handler):
|
||||
global profiler
|
||||
if not profiler:profiler=cProfile.Profile()
|
||||
profiler.enable();B=await handler(request);profiler.disable();A=pstats.Stats(profiler,stream=sys.stdout);A.sort_stats('cumulative');A.print_stats();return B
|
||||
async def profiler_handler(request):A=io.StringIO();B=pstats.Stats(profiler,stream=A);C=request.query.get('sort','tot. percall');B.sort_stats(C);B.print_stats();return web.Response(text=A.getvalue())
|
||||
async def profile_middleware(request, handler):
|
||||
global profiler
|
||||
if not profiler:
|
||||
profiler = cProfile.Profile()
|
||||
profiler.enable()
|
||||
response = await handler(request)
|
||||
profiler.disable()
|
||||
stats = pstats.Stats(profiler, stream=sys.stdout)
|
||||
stats.sort_stats("cumulative")
|
||||
stats.print_stats()
|
||||
return response
|
||||
|
||||
|
||||
async def profiler_handler(request):
|
||||
output = io.StringIO()
|
||||
stats = pstats.Stats(profiler, stream=output)
|
||||
sort_by = request.query.get("sort", "tot. percall")
|
||||
stats.sort_stats(sort_by)
|
||||
stats.print_stats()
|
||||
return web.Response(text=output.getvalue())
|
||||
|
||||
|
||||
class Profiler:
|
||||
def __init__(A):
|
||||
global profiler
|
||||
if profiler is None:profiler=cProfile.Profile()
|
||||
A.profiler=profiler
|
||||
async def __aenter__(A):A.profiler.enable()
|
||||
async def __aexit__(A,*B,**C):A.profiler.disable()
|
||||
|
||||
def __init__(self):
|
||||
global profiler
|
||||
if profiler is None:
|
||||
profiler = cProfile.Profile()
|
||||
self.profiler = profiler
|
||||
|
||||
async def __aenter__(self):
|
||||
self.profiler.enable()
|
||||
|
||||
async def __aexit__(self, *args, **kwargs):
|
||||
self.profiler.disable()
|
||||
|
||||
+81
-23
@@ -1,24 +1,82 @@
|
||||
_A='snekker-de-snek-'
|
||||
import hashlib,uuid
|
||||
DEFAULT_SALT=_A
|
||||
DEFAULT_NS=_A
|
||||
import hashlib
|
||||
import uuid
|
||||
|
||||
DEFAULT_SALT = "snekker-de-snek-"
|
||||
DEFAULT_NS = "snekker-de-snek-"
|
||||
|
||||
|
||||
class UIDNS:
|
||||
def __init__(A,name):'Initialize UIDNS with a name.';A.name=name
|
||||
@property
|
||||
def bytes(self):'Return the bytes representation of the name.';return self.name.encode()
|
||||
def uid(value=None,ns=DEFAULT_NS):
|
||||
'Generate a UUID based on the provided value and namespace.\n\n Args:\n value (str): The value to generate the UUID from. If None, a new UUID is created.\n ns (str): The namespace to use for UUID generation.\n\n Returns:\n str: The generated UUID as a string.\n ';A=value
|
||||
try:ns=ns.decode()
|
||||
except AttributeError:pass
|
||||
if not A:A=str(uuid.uuid4())
|
||||
try:A=A.decode()
|
||||
except AttributeError:pass
|
||||
return str(uuid.uuid5(UIDNS(ns),A))
|
||||
async def hash(data,salt=DEFAULT_SALT):
|
||||
'Hash the given data with the specified salt using SHA-256.\n\n Args:\n data (str): The data to hash.\n salt (str): The salt to use for hashing.\n\n Returns:\n str: The hexadecimal representation of the hashed data.\n ';C='ignore';A=salt;B=data
|
||||
try:B=B.encode(errors=C)
|
||||
except AttributeError:pass
|
||||
try:A=A.encode(errors=C)
|
||||
except AttributeError:pass
|
||||
D=A+B;E=hashlib.sha256(D);return E.hexdigest()
|
||||
async def verify(string,hashed):'Verify if the given string matches the hashed value.\n\n Args:\n string (str): The string to verify.\n hashed (str): The hashed value to compare against.\n\n Returns:\n bool: True if the string matches the hashed value, False otherwise.\n ';return await hash(string)==hashed
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialize UIDNS with a name."""
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def bytes(self) -> bytes:
|
||||
"""Return the bytes representation of the name."""
|
||||
return self.name.encode()
|
||||
|
||||
|
||||
def uid(value: str = None, ns: str = DEFAULT_NS) -> str:
|
||||
"""Generate a UUID based on the provided value and namespace.
|
||||
|
||||
Args:
|
||||
value (str): The value to generate the UUID from. If None, a new UUID is created.
|
||||
ns (str): The namespace to use for UUID generation.
|
||||
|
||||
Returns:
|
||||
str: The generated UUID as a string.
|
||||
"""
|
||||
try:
|
||||
ns = ns.decode()
|
||||
except AttributeError:
|
||||
pass
|
||||
if not value:
|
||||
value = str(uuid.uuid4())
|
||||
try:
|
||||
value = value.decode()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return str(uuid.uuid5(UIDNS(ns), value))
|
||||
|
||||
|
||||
def hash_sync(data: str, salt: str = DEFAULT_SALT) -> str:
|
||||
"""Hash the given data with the specified salt using SHA-256.
|
||||
|
||||
Args:
|
||||
data (str): The data to hash.
|
||||
salt (str): The salt to use for hashing.
|
||||
|
||||
Returns:
|
||||
str: The hexadecimal representation of the hashed data.
|
||||
"""
|
||||
try:
|
||||
data = data.encode(errors="ignore")
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
salt = salt.encode(errors="ignore")
|
||||
except AttributeError:
|
||||
pass
|
||||
salted = salt + data
|
||||
|
||||
obj = hashlib.sha256(salted)
|
||||
return obj.hexdigest()
|
||||
|
||||
async def hash(data: str, salt: str = DEFAULT_SALT) -> str:
|
||||
return hash_sync(data, salt)
|
||||
|
||||
def verify_sync(string: str, hashed: str) -> bool:
|
||||
"""Verify if the given string matches the hashed value.
|
||||
|
||||
Args:
|
||||
string (str): The string to verify.
|
||||
hashed (str): The hashed value to compare against.
|
||||
|
||||
Returns:
|
||||
bool: True if the string matches the hashed value, False otherwise.
|
||||
"""
|
||||
return hash_sync(string) == hashed
|
||||
|
||||
async def verify(string: str, hashed: str) -> bool:
|
||||
return verify_sync(string, hashed)
|
||||
|
||||
+63
-38
@@ -1,42 +1,67 @@
|
||||
_B='uid'
|
||||
_A=None
|
||||
from snek.mapper import get_mapper
|
||||
from snek.model.user import UserModel
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
|
||||
class BaseService:
|
||||
mapper_name:BaseMapper=_A
|
||||
@property
|
||||
def services(self):return self.app.services
|
||||
def __init__(A,app):
|
||||
A.app=app;A.cache=app.cache
|
||||
if A.mapper_name:A.mapper=get_mapper(A.mapper_name,app=A.app)
|
||||
else:A.mapper=_A
|
||||
async def exists(C,uid=_A,**A):
|
||||
B=uid
|
||||
if B:
|
||||
if not A and await C.cache.get(B):return True
|
||||
A[_B]=B
|
||||
return await C.count(**A)>0
|
||||
async def count(A,**B):return await A.mapper.count(**B)
|
||||
async def new(A,**B):return await A.mapper.new()
|
||||
async def query(A,sql,*B):
|
||||
for C in A.app.db.query(sql,*B):yield C
|
||||
async def get(B,uid=_A,**C):
|
||||
D=uid
|
||||
if D:
|
||||
if not C:
|
||||
A=await B.cache.get(D)
|
||||
if False and A and A.__class__==B.mapper.model_class:return A
|
||||
C[_B]=D
|
||||
A=await B.mapper.get(**C)
|
||||
if A:await B.cache.set(A[_B],A)
|
||||
return A
|
||||
async def save(B,model):
|
||||
A=model
|
||||
if await B.mapper.save(A):await B.cache.set(A[_B],A);return True
|
||||
C=await A.errors;raise Exception(f"Couldn't save model. Errors: f{C}")
|
||||
async def find(C,**A):
|
||||
B='_limit'
|
||||
if B not in A or int(A.get(B))>30:A[B]=60
|
||||
async for D in C.mapper.find(**A):yield D
|
||||
async def delete(A,**B):return await A.mapper.delete(**B)
|
||||
|
||||
mapper_name: BaseMapper = None
|
||||
|
||||
@property
|
||||
def services(self):
|
||||
return self.app.services
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.cache = app.cache
|
||||
if self.mapper_name:
|
||||
self.mapper = get_mapper(self.mapper_name, app=self.app)
|
||||
else:
|
||||
self.mapper = None
|
||||
|
||||
async def exists(self, uid=None, **kwargs):
|
||||
if uid:
|
||||
if not kwargs and await self.cache.get(uid):
|
||||
return True
|
||||
kwargs["uid"] = uid
|
||||
return await self.count(**kwargs) > 0
|
||||
|
||||
async def count(self, **kwargs):
|
||||
return await self.mapper.count(**kwargs)
|
||||
|
||||
async def new(self, **kwargs):
|
||||
return await self.mapper.new()
|
||||
|
||||
async def query(self, sql, *args):
|
||||
for record in self.app.db.query(sql, *args):
|
||||
yield record
|
||||
|
||||
async def get(self, uid=None, **kwargs):
|
||||
if uid:
|
||||
if not kwargs:
|
||||
result = await self.cache.get(uid)
|
||||
if False and result and result.__class__ == self.mapper.model_class:
|
||||
return result
|
||||
kwargs["uid"] = uid
|
||||
|
||||
result = await self.mapper.get(**kwargs)
|
||||
if result:
|
||||
await self.cache.set(result["uid"], result)
|
||||
return result
|
||||
|
||||
async def save(self, model: UserModel):
|
||||
# if model.is_valid: You Know why not
|
||||
if await self.mapper.save(model):
|
||||
await self.cache.set(model["uid"], model)
|
||||
return True
|
||||
errors = await model.errors
|
||||
raise Exception(f"Couldn't save model. Errors: f{errors}")
|
||||
|
||||
async def find(self, **kwargs):
|
||||
if "_limit" not in kwargs or int(kwargs.get("_limit")) > 30:
|
||||
kwargs["_limit"] = 60
|
||||
async for model in self.mapper.find(**kwargs):
|
||||
yield model
|
||||
|
||||
async def delete(self, **kwargs):
|
||||
return await self.mapper.delete(**kwargs)
|
||||
|
||||
+457
-68
File diff suppressed because one or more lines are too long
+111
-47
@@ -1,49 +1,113 @@
|
||||
_A=None
|
||||
import asyncio,os
|
||||
try:import pty
|
||||
except Exception as ex:print('You are not able to run a terminal. See error:');print(ex)
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
try:
|
||||
import pty
|
||||
except Exception as ex:
|
||||
print("You are not able to run a terminal. See error:")
|
||||
print(ex)
|
||||
import subprocess
|
||||
commands={'alpine':'docker run -it alpine /bin/sh','r':'docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh'}
|
||||
|
||||
commands = {
|
||||
"alpine": "docker run -it alpine /bin/sh",
|
||||
"r": "docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh",
|
||||
}
|
||||
|
||||
|
||||
class TerminalSession:
|
||||
def __init__(A,command):A.master,A.slave=_A,_A;A.process=_A;A.sockets=[];A.history=b'';A.history_size=20480;A.command=command;A.start_process(A.command)
|
||||
def start_process(A,command):
|
||||
if not A.is_running():
|
||||
if A.master:os.close(A.master);os.close(A.slave);A.master=_A;A.slave=_A
|
||||
A.master,A.slave=pty.openpty();A.process=subprocess.Popen(command.split(' '),stdin=A.slave,stdout=A.slave,stderr=A.slave,bufsize=0,universal_newlines=True)
|
||||
def is_running(A):
|
||||
if not A.process:return False
|
||||
asyncio.get_event_loop();return A.process.poll()is _A
|
||||
async def add_websocket(A,ws):A.start_process(A.command);asyncio.create_task(A.read_output(ws))
|
||||
async def read_output(A,ws):
|
||||
B=ws;A.sockets.append(B)
|
||||
if len(A.sockets)>1 and A.history:
|
||||
D=0
|
||||
try:D=A.history.index(b'\n')
|
||||
except ValueError:pass
|
||||
await B.send_bytes(A.history[D:]);return
|
||||
E=asyncio.get_event_loop()
|
||||
while True:
|
||||
try:
|
||||
C=await E.run_in_executor(_A,os.read,A.master,1024)
|
||||
if not C:break
|
||||
A.history+=C
|
||||
if len(A.history)>A.history_size:A.history=A.history[:0-A.history_size]
|
||||
try:
|
||||
for B in A.sockets:await B.send_bytes(C)
|
||||
except:A.sockets.remove(B)
|
||||
except Exception:await A.close();break
|
||||
async def close(A):
|
||||
print('Terminating process')
|
||||
if A.process:A.process.terminate();A.process=_A
|
||||
if A.master:os.close(A.master);os.close(A.slave);A.master=_A;A.slave=_A
|
||||
print('Terminated process')
|
||||
for B in A.sockets:
|
||||
try:await B.close()
|
||||
except Exception:pass
|
||||
A.sockets=[]
|
||||
async def write_input(B,data):
|
||||
A=data
|
||||
try:A=A.encode()
|
||||
except AttributeError:pass
|
||||
try:await asyncio.get_event_loop().run_in_executor(_A,os.write,B.master,A)
|
||||
except Exception as C:print(C);await B.close()
|
||||
def __init__(self, command):
|
||||
self.master, self.slave = None, None
|
||||
self.process = None
|
||||
self.sockets = []
|
||||
self.history = b""
|
||||
self.history_size = 1024 * 20
|
||||
self.command = command
|
||||
self.start_process(self.command)
|
||||
|
||||
def start_process(self, command):
|
||||
if not self.is_running():
|
||||
if self.master:
|
||||
os.close(self.master)
|
||||
os.close(self.slave)
|
||||
self.master = None
|
||||
self.slave = None
|
||||
|
||||
self.master, self.slave = pty.openpty()
|
||||
self.process = subprocess.Popen(
|
||||
command.split(" "),
|
||||
stdin=self.slave,
|
||||
stdout=self.slave,
|
||||
stderr=self.slave,
|
||||
bufsize=0,
|
||||
universal_newlines=True,
|
||||
)
|
||||
|
||||
def is_running(self):
|
||||
if not self.process:
|
||||
return False
|
||||
asyncio.get_event_loop()
|
||||
return self.process.poll() is None
|
||||
|
||||
async def add_websocket(self, ws):
|
||||
self.start_process(self.command)
|
||||
asyncio.create_task(self.read_output(ws))
|
||||
|
||||
async def read_output(self, ws):
|
||||
self.sockets.append(ws)
|
||||
if len(self.sockets) > 1 and self.history:
|
||||
start = 0
|
||||
try:
|
||||
start = self.history.index(b"\n")
|
||||
except ValueError:
|
||||
pass
|
||||
await ws.send_bytes(self.history[start:])
|
||||
return
|
||||
loop = asyncio.get_event_loop()
|
||||
while True:
|
||||
try:
|
||||
data = await loop.run_in_executor(None, os.read, self.master, 1024)
|
||||
if not data:
|
||||
break
|
||||
self.history += data
|
||||
if len(self.history) > self.history_size:
|
||||
self.history = self.history[: 0 - self.history_size]
|
||||
try:
|
||||
for ws in self.sockets:
|
||||
await ws.send_bytes(data) # Send raw bytes for ANSI support
|
||||
except:
|
||||
self.sockets.remove(ws)
|
||||
except Exception:
|
||||
await self.close()
|
||||
break
|
||||
|
||||
async def close(self):
|
||||
print("Terminating process")
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
self.process = None
|
||||
if self.master:
|
||||
os.close(self.master)
|
||||
os.close(self.slave)
|
||||
self.master = None
|
||||
self.slave = None
|
||||
|
||||
print("Terminated process")
|
||||
for ws in self.sockets:
|
||||
try:
|
||||
await ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.sockets = []
|
||||
|
||||
async def write_input(self, data):
|
||||
try:
|
||||
data = data.encode()
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
await asyncio.get_event_loop().run_in_executor(
|
||||
None, os.write, self.master, data
|
||||
)
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
await self.close()
|
||||
|
||||
+71
-27
@@ -1,31 +1,75 @@
|
||||
from aiohttp import web
|
||||
|
||||
from snek.system.markdown import render_markdown
|
||||
|
||||
|
||||
class BaseView(web.View):
|
||||
login_required=False
|
||||
async def _iter(A):
|
||||
if A.login_required and(not A.session.get('logged_in')or not A.session.get('uid')):return web.HTTPFound('/')
|
||||
return await super()._iter()
|
||||
@property
|
||||
def base_url(self):return str(self.request.url.with_path('').with_query(''))
|
||||
@property
|
||||
def app(self):return self.request.app
|
||||
@property
|
||||
def db(self):return self.app.db
|
||||
@property
|
||||
def services(self):return self.app.services
|
||||
async def json_response(B,data,**A):return web.json_response(data,**A)
|
||||
@property
|
||||
def session(self):return self.request.session
|
||||
async def render_template(A,template_name,context=None):
|
||||
C=context;B=template_name
|
||||
if B.endswith('.md'):D=await A.request.app.render_template(B,A.request,C);E=await render_markdown(A.app,D.body.decode());return web.Response(body=E,content_type='text/html')
|
||||
return await A.request.app.render_template(B,A.request,C)
|
||||
|
||||
login_required = False
|
||||
|
||||
async def _iter(self):
|
||||
if self.login_required and (
|
||||
not self.session.get("logged_in") or not self.session.get("uid")
|
||||
):
|
||||
return web.HTTPFound("/")
|
||||
return await super()._iter()
|
||||
|
||||
@property
|
||||
def base_url(self):
|
||||
return str(self.request.url.with_path("").with_query(""))
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
return self.request.app
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
return self.app.db
|
||||
|
||||
@property
|
||||
def services(self):
|
||||
return self.app.services
|
||||
|
||||
async def json_response(self, data, **kwargs):
|
||||
return web.json_response(data, **kwargs)
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
return self.request.session
|
||||
|
||||
async def render_template(self, template_name, context=None):
|
||||
if template_name.endswith(".md"):
|
||||
response = await self.request.app.render_template(
|
||||
template_name, self.request, context
|
||||
)
|
||||
body = await render_markdown(self.app, response.body.decode())
|
||||
return web.Response(body=body, content_type="text/html")
|
||||
return await self.request.app.render_template(
|
||||
template_name, self.request, context
|
||||
)
|
||||
|
||||
|
||||
class BaseFormView(BaseView):
|
||||
form=None
|
||||
async def get(A):B=A.form(app=A.app);return await A.json_response(await B.to_json())
|
||||
async def post(A):
|
||||
E='action';C=A.form(app=A.app);D=await A.request.json();C.set_user_data(D['form']);B=await C.to_json()
|
||||
if D.get(E)=='validate':0
|
||||
if D.get(E)=='submit'and B['is_valid']:B=await A.submit(C);return await A.json_response(B)
|
||||
return await A.json_response(B)
|
||||
async def submit(A,model=None):0
|
||||
|
||||
form = None
|
||||
|
||||
async def get(self):
|
||||
form = self.form(app=self.app)
|
||||
|
||||
return await self.json_response(await form.to_json())
|
||||
|
||||
async def post(self):
|
||||
form = self.form(app=self.app)
|
||||
post = await self.request.json()
|
||||
form.set_user_data(post["form"])
|
||||
result = await form.to_json()
|
||||
if post.get("action") == "validate":
|
||||
# Pass
|
||||
pass
|
||||
if post.get("action") == "submit" and result["is_valid"]:
|
||||
result = await self.submit(form)
|
||||
return await self.json_response(result)
|
||||
return await self.json_response(result)
|
||||
|
||||
async def submit(self, model=None):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
class WebSocketClient:
|
||||
def __init__(self, hostname, port):
|
||||
self.buffer = b''
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.hostname = hostname
|
||||
self.port = port
|
||||
self.connect()
|
||||
|
||||
def __getattr__(self, method, *args, **kwargs):
|
||||
if method in self.__dict__.keys():
|
||||
return self.__dict__[method]
|
||||
def call(*args, **kwargs):
|
||||
self.write(json.dumps({'method': method, 'args': args, 'kwargs': kwargs}))
|
||||
return json.loads(self.read())
|
||||
return call
|
||||
|
||||
def connect(self):
|
||||
self.socket.connect((self.hostname, self.port))
|
||||
key = base64.b64encode(b'1234123412341234').decode('utf-8')
|
||||
handshake = (
|
||||
f"GET /db HTTP/1.1\r\n"
|
||||
f"Host: localhost:3131\r\n"
|
||||
f"Upgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {key}\r\n"
|
||||
f"Sec-WebSocket-Version: 13\r\n\r\n"
|
||||
)
|
||||
self.socket.sendall(handshake.encode('utf-8'))
|
||||
response = self.read_until(b'\r\n\r\n')
|
||||
if b'101 Switching Protocols' not in response:
|
||||
raise Exception("Failed to connect to WebSocket")
|
||||
|
||||
def write(self, message):
|
||||
message_bytes = message.encode('utf-8')
|
||||
length = len(message_bytes)
|
||||
if length <= 125:
|
||||
self.socket.sendall(b'\x81' + bytes([length]) + message_bytes)
|
||||
elif length >= 126 and length <= 65535:
|
||||
self.socket.sendall(b'\x81' + bytes([126]) + length.to_bytes(2, 'big') + message_bytes)
|
||||
else:
|
||||
self.socket.sendall(b'\x81' + bytes([127]) + length.to_bytes(8, 'big') + message_bytes)
|
||||
|
||||
|
||||
def read_until(self, delimiter):
|
||||
while True:
|
||||
find_pos = self.buffer.find(delimiter)
|
||||
if find_pos != -1:
|
||||
data = self.buffer[:find_pos+4]
|
||||
self.buffer = self.buffer[find_pos+4:]
|
||||
return data
|
||||
|
||||
chunk = self.socket.recv(1024)
|
||||
if not chunk:
|
||||
return None
|
||||
self.buffer += chunk
|
||||
|
||||
def read_exactly(self, length):
|
||||
while len(self.buffer) < length:
|
||||
chunk = self.socket.recv(length - len(self.buffer))
|
||||
if not chunk:
|
||||
return None
|
||||
self.buffer += chunk
|
||||
response = self.buffer[: length]
|
||||
self.buffer = self.buffer[length:]
|
||||
return response
|
||||
|
||||
def read(self):
|
||||
frame = None
|
||||
frame = self.read_exactly(2)
|
||||
length = frame[1] & 127
|
||||
if length == 126:
|
||||
length = int.from_bytes(self.read_exactly(2), 'big')
|
||||
elif length == 127:
|
||||
length = int.from_bytes(self.read_exactly(8), 'big')
|
||||
message = self.read_exactly(length)
|
||||
return message
|
||||
|
||||
def close(self):
|
||||
self.socket.close()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user