Update.
This commit is contained in:
parent
30382b139d
commit
a3ab7b0586
@ -26,9 +26,9 @@ Description: Utility to load environment variables from a .env file.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
def load_env(file_path: str = '.env') -> None:
|
|
||||||
|
def load_env(file_path: str = ".env") -> None:
|
||||||
"""
|
"""
|
||||||
Loads environment variables from a specified file into the current process environment.
|
Loads environment variables from a specified file into the current process environment.
|
||||||
|
|
||||||
@ -43,26 +43,26 @@ def load_env(file_path: str = '.env') -> None:
|
|||||||
IOError: If an I/O error occurs during file reading.
|
IOError: If an I/O error occurs during file reading.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(file_path, 'r') as env_file:
|
with open(file_path) as env_file:
|
||||||
for line in env_file:
|
for line in env_file:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
# Skip empty lines and comments
|
# Skip empty lines and comments
|
||||||
if not line or line.startswith('#'):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
# Skip lines without '='
|
# Skip lines without '='
|
||||||
if '=' not in line:
|
if "=" not in line:
|
||||||
continue
|
continue
|
||||||
# Split into key and value at the first '='
|
# Split into key and value at the first '='
|
||||||
key, value = line.split('=', 1)
|
key, value = line.split("=", 1)
|
||||||
# Set environment variable
|
# Set environment variable
|
||||||
os.environ[key.strip()] = value.strip()
|
os.environ[key.strip()] = value.strip()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise FileNotFoundError(f"Environment file '{file_path}' not found.")
|
raise FileNotFoundError(f"Environment file '{file_path}' not found.")
|
||||||
except IOError as e:
|
except OSError as e:
|
||||||
raise IOError(f"Error reading environment file '{file_path}': {e}")
|
raise OSError(f"Error reading environment file '{file_path}': {e}")
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
load_env()
|
load_env()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
|
import asyncio
|
||||||
import pathlib
|
import pathlib
|
||||||
import shutil
|
import shutil
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import asyncio
|
|
||||||
import click
|
import click
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from IPython import start_ipython
|
|
||||||
from snek.shell import Shell
|
|
||||||
from snek.app import Application
|
from snek.app import Application
|
||||||
|
from snek.shell import Shell
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
def export():
|
def export():
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ def export():
|
|||||||
user = await app.services.user.get(uid=message["user_uid"])
|
user = await app.services.user.get(uid=message["user_uid"])
|
||||||
message["user"] = user and user["username"] or None
|
message["user"] = user and user["username"] or None
|
||||||
return (message["user"] or "") + ": " + (message["text"] or "")
|
return (message["user"] or "") + ": " + (message["text"] or "")
|
||||||
|
|
||||||
async def run():
|
async def run():
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
@ -49,6 +50,7 @@ def export():
|
|||||||
with open("dump.txt", "w") as f:
|
with open("dump.txt", "w") as f:
|
||||||
f.write("\n\n".join(result))
|
f.write("\n\n".join(result))
|
||||||
print("Dump written to dump.json")
|
print("Dump written to dump.json")
|
||||||
|
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|
||||||
@ -57,16 +59,20 @@ def statistics():
|
|||||||
async def run():
|
async def run():
|
||||||
app = Application(db_path="sqlite:///snek.db")
|
app = Application(db_path="sqlite:///snek.db")
|
||||||
app.services.statistics.database()
|
app.services.statistics.database()
|
||||||
|
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
def maintenance():
|
def maintenance():
|
||||||
async def run():
|
async def run():
|
||||||
app = Application(db_path="sqlite:///snek.db")
|
app = Application(db_path="sqlite:///snek.db")
|
||||||
await app.services.container.maintenance()
|
await app.services.container.maintenance()
|
||||||
await app.services.channel_message.maintenance()
|
await app.services.channel_message.maintenance()
|
||||||
|
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
"--db_path", default="snek.db", help="Database to initialize if not exists."
|
"--db_path", default="snek.db", help="Database to initialize if not exists."
|
||||||
@ -123,9 +129,11 @@ def serve(port, host, db_path):
|
|||||||
def shell(db_path):
|
def shell(db_path):
|
||||||
Shell(db_path).run()
|
Shell(db_path).run()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
|
|
||||||
sentry_sdk.init("https://ab6147c2f3354c819768c7e89455557b@gt.molodetz.nl/1")
|
sentry_sdk.init("https://ab6147c2f3354c819768c7e89455557b@gt.molodetz.nl/1")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("Could not import sentry_sdk")
|
print("Could not import sentry_sdk")
|
||||||
|
|||||||
@ -3,13 +3,13 @@ import logging
|
|||||||
import pathlib
|
import pathlib
|
||||||
import ssl
|
import ssl
|
||||||
import uuid
|
import uuid
|
||||||
import signal
|
|
||||||
from datetime import datetime
|
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from snek import snode
|
from snek import snode
|
||||||
from snek.view.threads import ThreadsView
|
|
||||||
from snek.system.ads import AsyncDataSet
|
from snek.system.ads import AsyncDataSet
|
||||||
|
from snek.view.threads import ThreadsView
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
@ -25,16 +25,21 @@ from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
|||||||
from app.app import Application as BaseApplication
|
from app.app import Application as BaseApplication
|
||||||
from jinja2 import FileSystemLoader
|
from jinja2 import FileSystemLoader
|
||||||
|
|
||||||
|
from snek.forum import setup_forum
|
||||||
from snek.mapper import get_mappers
|
from snek.mapper import get_mappers
|
||||||
from snek.service import get_services
|
from snek.service import get_services
|
||||||
from snek.sgit import GitApplication
|
from snek.sgit import GitApplication
|
||||||
from snek.sssh import start_ssh_server
|
from snek.sssh import start_ssh_server
|
||||||
from snek.system import http
|
from snek.system import http
|
||||||
from snek.system.cache import Cache
|
from snek.system.cache import Cache
|
||||||
from snek.system.stats import middleware as stats_middleware, create_stats_structure, stats_handler
|
|
||||||
from snek.system.markdown import MarkdownExtension
|
from snek.system.markdown import MarkdownExtension
|
||||||
from snek.system.middleware import auth_middleware, cors_middleware, csp_middleware
|
from snek.system.middleware import auth_middleware, cors_middleware, csp_middleware
|
||||||
from snek.system.profiler import profiler_handler
|
from snek.system.profiler import profiler_handler
|
||||||
|
from snek.system.stats import (
|
||||||
|
create_stats_structure,
|
||||||
|
middleware as stats_middleware,
|
||||||
|
stats_handler,
|
||||||
|
)
|
||||||
from snek.system.template import (
|
from snek.system.template import (
|
||||||
EmojiExtension,
|
EmojiExtension,
|
||||||
LinkifyExtension,
|
LinkifyExtension,
|
||||||
@ -43,10 +48,15 @@ from snek.system.template import (
|
|||||||
)
|
)
|
||||||
from snek.view.about import AboutHTMLView, AboutMDView
|
from snek.view.about import AboutHTMLView, AboutMDView
|
||||||
from snek.view.avatar import AvatarView
|
from snek.view.avatar import AvatarView
|
||||||
from snek.view.channel import ChannelAttachmentView,ChannelAttachmentUploadView, ChannelView
|
from snek.view.channel import (
|
||||||
|
ChannelAttachmentUploadView,
|
||||||
|
ChannelAttachmentView,
|
||||||
|
ChannelDriveApiView,
|
||||||
|
ChannelView,
|
||||||
|
)
|
||||||
|
from snek.view.container import ContainerView
|
||||||
from snek.view.docs import DocsHTMLView, DocsMDView
|
from snek.view.docs import DocsHTMLView, DocsMDView
|
||||||
from snek.view.drive import DriveApiView, DriveView
|
from snek.view.drive import DriveApiView, DriveView
|
||||||
from snek.view.channel import ChannelDriveApiView
|
|
||||||
from snek.view.index import IndexView
|
from snek.view.index import IndexView
|
||||||
from snek.view.login import LoginView
|
from snek.view.login import LoginView
|
||||||
from snek.view.logout import LogoutView
|
from snek.view.logout import LogoutView
|
||||||
@ -55,7 +65,6 @@ from snek.view.register import RegisterView
|
|||||||
from snek.view.repository import RepositoryView
|
from snek.view.repository import RepositoryView
|
||||||
from snek.view.rpc import RPCView
|
from snek.view.rpc import RPCView
|
||||||
from snek.view.search_user import SearchUserView
|
from snek.view.search_user import SearchUserView
|
||||||
from snek.view.container import ContainerView
|
|
||||||
from snek.view.settings.containers import (
|
from snek.view.settings.containers import (
|
||||||
ContainersCreateView,
|
ContainersCreateView,
|
||||||
ContainersDeleteView,
|
ContainersDeleteView,
|
||||||
@ -77,7 +86,6 @@ from snek.view.upload import UploadView
|
|||||||
from snek.view.user import UserView
|
from snek.view.user import UserView
|
||||||
from snek.view.web import WebView
|
from snek.view.web import WebView
|
||||||
from snek.webdav import WebdavApplication
|
from snek.webdav import WebdavApplication
|
||||||
from snek.forum import setup_forum
|
|
||||||
|
|
||||||
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
|
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
|
||||||
from snek.system.template import whitelist_attributes
|
from snek.system.template import whitelist_attributes
|
||||||
@ -175,13 +183,12 @@ class Application(BaseApplication):
|
|||||||
self.on_startup.append(self.prepare_asyncio)
|
self.on_startup.append(self.prepare_asyncio)
|
||||||
self.on_startup.append(self.start_user_availability_service)
|
self.on_startup.append(self.start_user_availability_service)
|
||||||
self.on_startup.append(self.start_ssh_server)
|
self.on_startup.append(self.start_ssh_server)
|
||||||
#self.on_startup.append(self.prepare_database)
|
# self.on_startup.append(self.prepare_database)
|
||||||
|
|
||||||
async def prepare_stats(self, app):
|
async def prepare_stats(self, app):
|
||||||
app['stats'] = create_stats_structure()
|
app["stats"] = create_stats_structure()
|
||||||
print("Stats prepared", flush=True)
|
print("Stats prepared", flush=True)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uptime_seconds(self):
|
def uptime_seconds(self):
|
||||||
return (datetime.now() - self.time_start).total_seconds()
|
return (datetime.now() - self.time_start).total_seconds()
|
||||||
@ -225,10 +232,10 @@ class Application(BaseApplication):
|
|||||||
# app.loop = asyncio.get_running_loop()
|
# app.loop = asyncio.get_running_loop()
|
||||||
app.executor = ThreadPoolExecutor(max_workers=200)
|
app.executor = ThreadPoolExecutor(max_workers=200)
|
||||||
app.loop.set_default_executor(self.executor)
|
app.loop.set_default_executor(self.executor)
|
||||||
#for sig in (signal.SIGINT, signal.SIGTERM):
|
# for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
#app.loop.add_signal_handler(
|
# app.loop.add_signal_handler(
|
||||||
# sig, lambda: asyncio.create_task(self.services.container.shutdown())
|
# sig, lambda: asyncio.create_task(self.services.container.shutdown())
|
||||||
#)
|
# )
|
||||||
|
|
||||||
async def create_task(self, task):
|
async def create_task(self, task):
|
||||||
await self.tasks.put(task)
|
await self.tasks.put(task)
|
||||||
@ -244,7 +251,6 @@ class Application(BaseApplication):
|
|||||||
print(ex)
|
print(ex)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
|
|
||||||
async def prepare_database(self, app):
|
async def prepare_database(self, app):
|
||||||
await self.db.query_raw("PRAGMA journal_mode=WAL")
|
await self.db.query_raw("PRAGMA journal_mode=WAL")
|
||||||
await self.db.query_raw("PRAGMA syncnorm=off")
|
await self.db.query_raw("PRAGMA syncnorm=off")
|
||||||
@ -291,9 +297,7 @@ class Application(BaseApplication):
|
|||||||
self.router.add_view(
|
self.router.add_view(
|
||||||
"/channel/{channel_uid}/attachment.bin", ChannelAttachmentView
|
"/channel/{channel_uid}/attachment.bin", ChannelAttachmentView
|
||||||
)
|
)
|
||||||
self.router.add_view(
|
self.router.add_view("/channel/{channel_uid}/drive.json", ChannelDriveApiView)
|
||||||
"/channel/{channel_uid}/drive.json", ChannelDriveApiView
|
|
||||||
)
|
|
||||||
self.router.add_view(
|
self.router.add_view(
|
||||||
"/channel/{channel_uid}/attachment.sock", ChannelAttachmentUploadView
|
"/channel/{channel_uid}/attachment.sock", ChannelAttachmentUploadView
|
||||||
)
|
)
|
||||||
@ -414,9 +418,9 @@ class Application(BaseApplication):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
context["nonce"] = request['csp_nonce']
|
context["nonce"] = request["csp_nonce"]
|
||||||
except:
|
except:
|
||||||
context['nonce'] = '?'
|
context["nonce"] = "?"
|
||||||
|
|
||||||
rendered = await super().render_template(template, request, context)
|
rendered = await super().render_template(template, request, context)
|
||||||
|
|
||||||
@ -466,14 +470,14 @@ class Application(BaseApplication):
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def no_save(self):
|
async def no_save(self):
|
||||||
stats = {
|
stats = {"count": 0}
|
||||||
'count': 0
|
|
||||||
}
|
|
||||||
async def patched_save(*args, **kwargs):
|
async def patched_save(*args, **kwargs):
|
||||||
await self.cache.set(args[0]["uid"], args[0])
|
await self.cache.set(args[0]["uid"], args[0])
|
||||||
stats['count'] = stats['count'] + 1
|
stats["count"] = stats["count"] + 1
|
||||||
print(f"save is ignored {stats['count']} times")
|
print(f"save is ignored {stats['count']} times")
|
||||||
return args[0]
|
return args[0]
|
||||||
|
|
||||||
save_original = self.services.channel_message.mapper.save
|
save_original = self.services.channel_message.mapper.save
|
||||||
self.services.channel_message.mapper.save = patched_save
|
self.services.channel_message.mapper.save = patched_save
|
||||||
raised_exception = None
|
raised_exception = None
|
||||||
@ -486,6 +490,7 @@ class Application(BaseApplication):
|
|||||||
if raised_exception:
|
if raised_exception:
|
||||||
raise raised_exception
|
raise raised_exception
|
||||||
|
|
||||||
|
|
||||||
app = Application(db_path="sqlite:///snek.db")
|
app = Application(db_path="sqlite:///snek.db")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
# forum_app.py
|
# forum_app.py
|
||||||
import aiohttp.web
|
import aiohttp.web
|
||||||
|
|
||||||
from snek.view.forum import ForumIndexView, ForumView, ForumWebSocketView
|
from snek.view.forum import ForumIndexView, ForumView, ForumWebSocketView
|
||||||
|
|
||||||
|
|
||||||
@ -7,13 +8,13 @@ class ForumApplication(aiohttp.web.Application):
|
|||||||
def __init__(self, parent, *args, **kwargs):
|
def __init__(self, parent, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.render_template = self.parent.render_template
|
self.render_template = self.parent.render_template
|
||||||
# Set up routes
|
# Set up routes
|
||||||
self.setup_routes()
|
self.setup_routes()
|
||||||
|
|
||||||
# Set up notification listeners
|
# Set up notification listeners
|
||||||
self.setup_notifications()
|
self.setup_notifications()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def db(self):
|
def db(self):
|
||||||
return self.parent.db
|
return self.parent.db
|
||||||
@ -21,86 +22,84 @@ class ForumApplication(aiohttp.web.Application):
|
|||||||
@property
|
@property
|
||||||
def services(self):
|
def services(self):
|
||||||
return self.parent.services
|
return self.parent.services
|
||||||
|
|
||||||
def setup_routes(self):
|
def setup_routes(self):
|
||||||
"""Set up all forum routes"""
|
"""Set up all forum routes"""
|
||||||
# API routes
|
# API routes
|
||||||
self.router.add_view("/index.html", ForumIndexView)
|
self.router.add_view("/index.html", ForumIndexView)
|
||||||
self.router.add_route("GET", "/api/forums", ForumView.get_forums)
|
self.router.add_route("GET", "/api/forums", ForumView.get_forums)
|
||||||
self.router.add_route("GET", "/api/forums/{slug}", ForumView.get_forum)
|
self.router.add_route("GET", "/api/forums/{slug}", ForumView.get_forum)
|
||||||
self.router.add_route("POST", "/api/forums/{slug}/threads", ForumView.create_thread)
|
self.router.add_route(
|
||||||
|
"POST", "/api/forums/{slug}/threads", ForumView.create_thread
|
||||||
|
)
|
||||||
self.router.add_route("GET", "/api/threads/{thread_slug}", ForumView.get_thread)
|
self.router.add_route("GET", "/api/threads/{thread_slug}", ForumView.get_thread)
|
||||||
self.router.add_route("POST", "/api/threads/{thread_uid}/posts", ForumView.create_post)
|
self.router.add_route(
|
||||||
|
"POST", "/api/threads/{thread_uid}/posts", ForumView.create_post
|
||||||
|
)
|
||||||
self.router.add_route("PUT", "/api/posts/{post_uid}", ForumView.edit_post)
|
self.router.add_route("PUT", "/api/posts/{post_uid}", ForumView.edit_post)
|
||||||
self.router.add_route("DELETE", "/api/posts/{post_uid}", ForumView.delete_post)
|
self.router.add_route("DELETE", "/api/posts/{post_uid}", ForumView.delete_post)
|
||||||
self.router.add_route("POST", "/api/posts/{post_uid}/like", ForumView.toggle_like)
|
self.router.add_route(
|
||||||
self.router.add_route("POST", "/api/threads/{thread_uid}/pin", ForumView.toggle_pin)
|
"POST", "/api/posts/{post_uid}/like", ForumView.toggle_like
|
||||||
self.router.add_route("POST", "/api/threads/{thread_uid}/lock", ForumView.toggle_lock)
|
)
|
||||||
|
self.router.add_route(
|
||||||
|
"POST", "/api/threads/{thread_uid}/pin", ForumView.toggle_pin
|
||||||
|
)
|
||||||
|
self.router.add_route(
|
||||||
|
"POST", "/api/threads/{thread_uid}/lock", ForumView.toggle_lock
|
||||||
|
)
|
||||||
|
|
||||||
# WebSocket route
|
# WebSocket route
|
||||||
self.router.add_view("/ws", ForumWebSocketView)
|
self.router.add_view("/ws", ForumWebSocketView)
|
||||||
|
|
||||||
# Static HTML route
|
# Static HTML route
|
||||||
self.router.add_route("GET", "/{path:.*}", self.serve_forum_html)
|
self.router.add_route("GET", "/{path:.*}", self.serve_forum_html)
|
||||||
|
|
||||||
def setup_notifications(self):
|
def setup_notifications(self):
|
||||||
"""Set up notification listeners for WebSocket broadcasting"""
|
"""Set up notification listeners for WebSocket broadcasting"""
|
||||||
# Forum notifications
|
# Forum notifications
|
||||||
self.services.forum.add_notification_listener("forum_created", self.on_forum_event)
|
self.services.forum.add_notification_listener(
|
||||||
|
"forum_created", self.on_forum_event
|
||||||
|
)
|
||||||
|
|
||||||
# Thread notifications
|
# Thread notifications
|
||||||
self.services.thread.add_notification_listener("thread_created", self.on_thread_event)
|
self.services.thread.add_notification_listener(
|
||||||
|
"thread_created", self.on_thread_event
|
||||||
|
)
|
||||||
|
|
||||||
# Post notifications
|
# Post notifications
|
||||||
self.services.post.add_notification_listener("post_created", self.on_post_event)
|
self.services.post.add_notification_listener("post_created", self.on_post_event)
|
||||||
self.services.post.add_notification_listener("post_edited", self.on_post_event)
|
self.services.post.add_notification_listener("post_edited", self.on_post_event)
|
||||||
self.services.post.add_notification_listener("post_deleted", self.on_post_event)
|
self.services.post.add_notification_listener("post_deleted", self.on_post_event)
|
||||||
|
|
||||||
# Like notifications
|
# Like notifications
|
||||||
self.services.post_like.add_notification_listener("post_liked", self.on_like_event)
|
self.services.post_like.add_notification_listener(
|
||||||
self.services.post_like.add_notification_listener("post_unliked", self.on_like_event)
|
"post_liked", self.on_like_event
|
||||||
|
)
|
||||||
|
self.services.post_like.add_notification_listener(
|
||||||
|
"post_unliked", self.on_like_event
|
||||||
|
)
|
||||||
|
|
||||||
async def on_forum_event(self, event_type, data):
|
async def on_forum_event(self, event_type, data):
|
||||||
"""Handle forum events"""
|
"""Handle forum events"""
|
||||||
await ForumWebSocketView.broadcast_update(self, event_type, data)
|
await ForumWebSocketView.broadcast_update(self, event_type, data)
|
||||||
|
|
||||||
async def on_thread_event(self, event_type, data):
|
async def on_thread_event(self, event_type, data):
|
||||||
"""Handle thread events"""
|
"""Handle thread events"""
|
||||||
await ForumWebSocketView.broadcast_update(self, event_type, data)
|
await ForumWebSocketView.broadcast_update(self, event_type, data)
|
||||||
|
|
||||||
async def on_post_event(self, event_type, data):
|
async def on_post_event(self, event_type, data):
|
||||||
"""Handle post events"""
|
"""Handle post events"""
|
||||||
await ForumWebSocketView.broadcast_update(self, event_type, data)
|
await ForumWebSocketView.broadcast_update(self, event_type, data)
|
||||||
|
|
||||||
async def on_like_event(self, event_type, data):
|
async def on_like_event(self, event_type, data):
|
||||||
"""Handle like events"""
|
"""Handle like events"""
|
||||||
await ForumWebSocketView.broadcast_update(self, event_type, data)
|
await ForumWebSocketView.broadcast_update(self, event_type, data)
|
||||||
|
|
||||||
async def serve_forum_html(self, request):
|
async def serve_forum_html(self, request):
|
||||||
"""Serve the forum HTML with the web component"""
|
"""Serve the forum HTML with the web component"""
|
||||||
html = """<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Forum</title>
|
|
||||||
<script type="module" src="/forum/static/snek-forum.js"></script>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<snek-forum></snek-forum>
|
|
||||||
</body>
|
|
||||||
</html>"""
|
|
||||||
return await self.parent.render_template("forum.html", request)
|
return await self.parent.render_template("forum.html", request)
|
||||||
|
|
||||||
|
# return aiohttp.web.Response(text=html, content_type="text/html")
|
||||||
#return aiohttp.web.Response(text=html, content_type="text/html")
|
|
||||||
|
|
||||||
|
|
||||||
# Integration with main app
|
# Integration with main app
|
||||||
|
|||||||
@ -7,12 +7,12 @@ from snek.mapper.channel_message import ChannelMessageMapper
|
|||||||
from snek.mapper.container import ContainerMapper
|
from snek.mapper.container import ContainerMapper
|
||||||
from snek.mapper.drive import DriveMapper
|
from snek.mapper.drive import DriveMapper
|
||||||
from snek.mapper.drive_item import DriveItemMapper
|
from snek.mapper.drive_item import DriveItemMapper
|
||||||
|
from snek.mapper.forum import ForumMapper, PostLikeMapper, PostMapper, ThreadMapper
|
||||||
from snek.mapper.notification import NotificationMapper
|
from snek.mapper.notification import NotificationMapper
|
||||||
from snek.mapper.push import PushMapper
|
from snek.mapper.push import PushMapper
|
||||||
from snek.mapper.repository import RepositoryMapper
|
from snek.mapper.repository import RepositoryMapper
|
||||||
from snek.mapper.user import UserMapper
|
from snek.mapper.user import UserMapper
|
||||||
from snek.mapper.user_property import UserPropertyMapper
|
from snek.mapper.user_property import UserPropertyMapper
|
||||||
from snek.mapper.forum import ForumMapper, ThreadMapper, PostMapper, PostLikeMapper
|
|
||||||
from snek.system.object import Object
|
from snek.system.object import Object
|
||||||
|
|
||||||
|
|
||||||
@ -42,4 +42,3 @@ def get_mappers(app=None):
|
|||||||
|
|
||||||
def get_mapper(name, app=None):
|
def get_mapper(name, app=None):
|
||||||
return get_mappers(app=app)[name]
|
return get_mappers(app=app)[name]
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# mapper/forum.py
|
# mapper/forum.py
|
||||||
from snek.model.forum import ForumModel, ThreadModel, PostModel, PostLikeModel
|
from snek.model.forum import ForumModel, PostLikeModel, PostModel, ThreadModel
|
||||||
from snek.system.mapper import BaseMapper
|
from snek.system.mapper import BaseMapper
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,12 +9,12 @@ from snek.model.channel_message import ChannelMessageModel
|
|||||||
from snek.model.container import Container
|
from snek.model.container import Container
|
||||||
from snek.model.drive import DriveModel
|
from snek.model.drive import DriveModel
|
||||||
from snek.model.drive_item import DriveItemModel
|
from snek.model.drive_item import DriveItemModel
|
||||||
|
from snek.model.forum import ForumModel, PostLikeModel, PostModel, ThreadModel
|
||||||
from snek.model.notification import NotificationModel
|
from snek.model.notification import NotificationModel
|
||||||
from snek.model.push_registration import PushRegistrationModel
|
from snek.model.push_registration import PushRegistrationModel
|
||||||
from snek.model.repository import RepositoryModel
|
from snek.model.repository import RepositoryModel
|
||||||
from snek.model.user import UserModel
|
from snek.model.user import UserModel
|
||||||
from snek.model.user_property import UserPropertyModel
|
from snek.model.user_property import UserPropertyModel
|
||||||
from snek.model.forum import ForumModel, ThreadModel, PostModel, PostLikeModel
|
|
||||||
from snek.system.object import Object
|
from snek.system.object import Object
|
||||||
|
|
||||||
|
|
||||||
@ -44,5 +44,3 @@ def get_models():
|
|||||||
|
|
||||||
def get_model(name):
|
def get_model(name):
|
||||||
return get_models()[name]
|
return get_models()[name]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -12,10 +12,10 @@ class ChannelModel(BaseModel):
|
|||||||
index = ModelField(name="index", required=True, kind=int, value=1000)
|
index = ModelField(name="index", required=True, kind=int, value=1000)
|
||||||
last_message_on = ModelField(name="last_message_on", required=False, kind=str)
|
last_message_on = ModelField(name="last_message_on", required=False, kind=str)
|
||||||
history_start = ModelField(name="history_start", required=False, kind=str)
|
history_start = ModelField(name="history_start", required=False, kind=str)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_dm(self):
|
def is_dm(self):
|
||||||
return 'dm' in self['tag'].lower()
|
return "dm" in self["tag"].lower()
|
||||||
|
|
||||||
async def get_last_message(self) -> ChannelMessageModel:
|
async def get_last_message(self) -> ChannelMessageModel:
|
||||||
history_start_filter = ""
|
history_start_filter = ""
|
||||||
@ -23,7 +23,9 @@ class ChannelModel(BaseModel):
|
|||||||
history_start_filter = f" AND created_at > '{self['history_start']}' "
|
history_start_filter = f" AND created_at > '{self['history_start']}' "
|
||||||
try:
|
try:
|
||||||
async for model in self.app.services.channel_message.query(
|
async for model in self.app.services.channel_message.query(
|
||||||
"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid" + history_start_filter + " ORDER BY created_at DESC LIMIT 1",
|
"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid"
|
||||||
|
+ history_start_filter
|
||||||
|
+ " ORDER BY created_at DESC LIMIT 1",
|
||||||
{"channel_uid": self["uid"]},
|
{"channel_uid": self["uid"]},
|
||||||
):
|
):
|
||||||
|
|
||||||
|
|||||||
@ -4,9 +4,16 @@ from snek.system.model import BaseModel, ModelField
|
|||||||
|
|
||||||
class ForumModel(BaseModel):
|
class ForumModel(BaseModel):
|
||||||
"""Forum categories"""
|
"""Forum categories"""
|
||||||
name = ModelField(name="name", required=True, kind=str, min_length=3, max_length=100)
|
|
||||||
description = ModelField(name="description", required=False, kind=str, max_length=500)
|
name = ModelField(
|
||||||
slug = ModelField(name="slug", required=True, kind=str, regex=r"^[a-z0-9-]+$", unique=True)
|
name="name", required=True, kind=str, min_length=3, max_length=100
|
||||||
|
)
|
||||||
|
description = ModelField(
|
||||||
|
name="description", required=False, kind=str, max_length=500
|
||||||
|
)
|
||||||
|
slug = ModelField(
|
||||||
|
name="slug", required=True, kind=str, regex=r"^[a-z0-9-]+$", unique=True
|
||||||
|
)
|
||||||
icon = ModelField(name="icon", required=False, kind=str)
|
icon = ModelField(name="icon", required=False, kind=str)
|
||||||
position = ModelField(name="position", required=True, kind=int, value=0)
|
position = ModelField(name="position", required=True, kind=int, value=0)
|
||||||
is_active = ModelField(name="is_active", required=True, kind=bool, value=True)
|
is_active = ModelField(name="is_active", required=True, kind=bool, value=True)
|
||||||
@ -18,12 +25,12 @@ class ForumModel(BaseModel):
|
|||||||
|
|
||||||
async def get_threads(self, limit=50, offset=0):
|
async def get_threads(self, limit=50, offset=0):
|
||||||
async for thread in self.app.services.thread.find(
|
async for thread in self.app.services.thread.find(
|
||||||
forum_uid=self["uid"],
|
forum_uid=self["uid"],
|
||||||
deleted_at=None,
|
deleted_at=None,
|
||||||
_limit=limit,
|
_limit=limit,
|
||||||
_offset=offset,
|
_offset=offset,
|
||||||
order_by="-last_post_at"
|
order_by="-last_post_at",
|
||||||
#order_by="is_pinned DESC, last_post_at DESC"
|
# order_by="is_pinned DESC, last_post_at DESC"
|
||||||
):
|
):
|
||||||
yield thread
|
yield thread
|
||||||
|
|
||||||
@ -39,8 +46,11 @@ class ForumModel(BaseModel):
|
|||||||
# models/thread.py
|
# models/thread.py
|
||||||
class ThreadModel(BaseModel):
|
class ThreadModel(BaseModel):
|
||||||
"""Forum threads"""
|
"""Forum threads"""
|
||||||
|
|
||||||
forum_uid = ModelField(name="forum_uid", required=True, kind=str)
|
forum_uid = ModelField(name="forum_uid", required=True, kind=str)
|
||||||
title = ModelField(name="title", required=True, kind=str, min_length=5, max_length=200)
|
title = ModelField(
|
||||||
|
name="title", required=True, kind=str, min_length=5, max_length=200
|
||||||
|
)
|
||||||
slug = ModelField(name="slug", required=True, kind=str, regex=r"^[a-z0-9-]+$")
|
slug = ModelField(name="slug", required=True, kind=str, regex=r"^[a-z0-9-]+$")
|
||||||
created_by_uid = ModelField(name="created_by_uid", required=True, kind=str)
|
created_by_uid = ModelField(name="created_by_uid", required=True, kind=str)
|
||||||
is_pinned = ModelField(name="is_pinned", required=True, kind=bool, value=False)
|
is_pinned = ModelField(name="is_pinned", required=True, kind=bool, value=False)
|
||||||
@ -56,7 +66,7 @@ class ThreadModel(BaseModel):
|
|||||||
deleted_at=None,
|
deleted_at=None,
|
||||||
_limit=limit,
|
_limit=limit,
|
||||||
_offset=offset,
|
_offset=offset,
|
||||||
order_by="created_at"
|
order_by="created_at",
|
||||||
):
|
):
|
||||||
yield post
|
yield post
|
||||||
|
|
||||||
@ -70,30 +80,35 @@ class ThreadModel(BaseModel):
|
|||||||
await self.save()
|
await self.save()
|
||||||
|
|
||||||
|
|
||||||
# models/post.py
|
# models/post.py
|
||||||
class PostModel(BaseModel):
|
class PostModel(BaseModel):
|
||||||
"""Forum posts"""
|
"""Forum posts"""
|
||||||
|
|
||||||
thread_uid = ModelField(name="thread_uid", required=True, kind=str)
|
thread_uid = ModelField(name="thread_uid", required=True, kind=str)
|
||||||
forum_uid = ModelField(name="forum_uid", required=True, kind=str)
|
forum_uid = ModelField(name="forum_uid", required=True, kind=str)
|
||||||
content = ModelField(name="content", required=True, kind=str, min_length=1, max_length=10000)
|
content = ModelField(
|
||||||
|
name="content", required=True, kind=str, min_length=1, max_length=10000
|
||||||
|
)
|
||||||
created_by_uid = ModelField(name="created_by_uid", required=True, kind=str)
|
created_by_uid = ModelField(name="created_by_uid", required=True, kind=str)
|
||||||
edited_at = ModelField(name="edited_at", required=False, kind=str)
|
edited_at = ModelField(name="edited_at", required=False, kind=str)
|
||||||
edited_by_uid = ModelField(name="edited_by_uid", required=False, kind=str)
|
edited_by_uid = ModelField(name="edited_by_uid", required=False, kind=str)
|
||||||
is_first_post = ModelField(name="is_first_post", required=True, kind=bool, value=False)
|
is_first_post = ModelField(
|
||||||
|
name="is_first_post", required=True, kind=bool, value=False
|
||||||
|
)
|
||||||
like_count = ModelField(name="like_count", required=True, kind=int, value=0)
|
like_count = ModelField(name="like_count", required=True, kind=int, value=0)
|
||||||
|
|
||||||
async def get_author(self):
|
async def get_author(self):
|
||||||
return await self.app.services.user.get(uid=self["created_by_uid"])
|
return await self.app.services.user.get(uid=self["created_by_uid"])
|
||||||
|
|
||||||
async def is_liked_by(self, user_uid):
|
async def is_liked_by(self, user_uid):
|
||||||
return await self.app.services.post_like.exists(
|
return await self.app.services.post_like.exists(
|
||||||
post_uid=self["uid"],
|
post_uid=self["uid"], user_uid=user_uid
|
||||||
user_uid=user_uid
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# models/post_like.py
|
# models/post_like.py
|
||||||
class PostLikeModel(BaseModel):
|
class PostLikeModel(BaseModel):
|
||||||
"""Post likes"""
|
"""Post likes"""
|
||||||
|
|
||||||
post_uid = ModelField(name="post_uid", required=True, kind=str)
|
post_uid = ModelField(name="post_uid", required=True, kind=str)
|
||||||
user_uid = ModelField(name="user_uid", required=True, kind=str)
|
user_uid = ModelField(name="user_uid", required=True, kind=str)
|
||||||
|
|||||||
@ -9,37 +9,43 @@ from snek.service.container import ContainerService
|
|||||||
from snek.service.db import DBService
|
from snek.service.db import DBService
|
||||||
from snek.service.drive import DriveService
|
from snek.service.drive import DriveService
|
||||||
from snek.service.drive_item import DriveItemService
|
from snek.service.drive_item import DriveItemService
|
||||||
|
from snek.service.forum import ForumService, PostLikeService, PostService, ThreadService
|
||||||
from snek.service.notification import NotificationService
|
from snek.service.notification import NotificationService
|
||||||
from snek.service.push import PushService
|
from snek.service.push import PushService
|
||||||
from snek.service.repository import RepositoryService
|
from snek.service.repository import RepositoryService
|
||||||
from snek.service.socket import SocketService
|
from snek.service.socket import SocketService
|
||||||
|
from snek.service.statistics import StatisticsService
|
||||||
from snek.service.user import UserService
|
from snek.service.user import UserService
|
||||||
from snek.service.user_property import UserPropertyService
|
from snek.service.user_property import UserPropertyService
|
||||||
from snek.service.util import UtilService
|
from snek.service.util import UtilService
|
||||||
from snek.system.object import Object
|
from snek.system.object import Object
|
||||||
from snek.service.statistics import StatisticsService
|
|
||||||
from snek.service.forum import ForumService, ThreadService, PostService, PostLikeService
|
|
||||||
_service_registry = {}
|
_service_registry = {}
|
||||||
|
|
||||||
|
|
||||||
def register_service(name, service_cls):
|
def register_service(name, service_cls):
|
||||||
_service_registry[name] = service_cls
|
_service_registry[name] = service_cls
|
||||||
|
|
||||||
|
|
||||||
register = register_service
|
register = register_service
|
||||||
|
|
||||||
|
|
||||||
@functools.cache
|
@functools.cache
|
||||||
def get_services(app):
|
def get_services(app):
|
||||||
result = Object(
|
result = Object(
|
||||||
**{
|
**{
|
||||||
name: service_cls(app=app)
|
name: service_cls(app=app)
|
||||||
for name, service_cls in _service_registry.items()
|
for name, service_cls in _service_registry.items()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
result.register = register_service
|
result.register = register_service
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_service(name, app=None):
|
def get_service(name, app=None):
|
||||||
return get_services(app=app)[name]
|
return get_services(app=app)[name]
|
||||||
|
|
||||||
|
|
||||||
# Registering all services
|
# Registering all services
|
||||||
register_service("user", UserService)
|
register_service("user", UserService)
|
||||||
register_service("channel_member", ChannelMemberService)
|
register_service("channel_member", ChannelMemberService)
|
||||||
@ -62,4 +68,3 @@ register_service("forum", ForumService)
|
|||||||
register_service("thread", ThreadService)
|
register_service("thread", ThreadService)
|
||||||
register_service("post", PostService)
|
register_service("post", PostService)
|
||||||
register_service("post_like", PostLikeService)
|
register_service("post_like", PostLikeService)
|
||||||
|
|
||||||
|
|||||||
@ -115,10 +115,9 @@ class ChannelService(BaseService):
|
|||||||
|
|
||||||
async def clear(self, channel_uid):
|
async def clear(self, channel_uid):
|
||||||
model = await self.get(uid=channel_uid)
|
model = await self.get(uid=channel_uid)
|
||||||
model['history_from'] = datetime.now()
|
model["history_from"] = datetime.now()
|
||||||
await self.save(model)
|
await self.save(model)
|
||||||
|
|
||||||
|
|
||||||
async def ensure_public_channel(self, created_by_uid):
|
async def ensure_public_channel(self, created_by_uid):
|
||||||
model = await self.get(is_listed=True, tag="public")
|
model = await self.get(is_listed=True, tag="public")
|
||||||
is_moderator = False
|
is_moderator = False
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
from snek.system.template import sanitize_html
|
from snek.system.template import sanitize_html
|
||||||
import time
|
|
||||||
|
|
||||||
class ChannelMessageService(BaseService):
|
class ChannelMessageService(BaseService):
|
||||||
mapper_name = "channel_message"
|
mapper_name = "channel_message"
|
||||||
@ -10,7 +12,6 @@ class ChannelMessageService(BaseService):
|
|||||||
self._configured_indexes = False
|
self._configured_indexes = False
|
||||||
|
|
||||||
async def maintenance(self):
|
async def maintenance(self):
|
||||||
args = {}
|
|
||||||
for message in self.mapper.db["channel_message"].find():
|
for message in self.mapper.db["channel_message"].find():
|
||||||
print(message)
|
print(message)
|
||||||
try:
|
try:
|
||||||
@ -34,7 +35,6 @@ class ChannelMessageService(BaseService):
|
|||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
print(ex, flush=True)
|
print(ex, flush=True)
|
||||||
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
changed = 0
|
changed = 0
|
||||||
async for message in self.find(is_final=False):
|
async for message in self.find(is_final=False):
|
||||||
@ -72,7 +72,7 @@ class ChannelMessageService(BaseService):
|
|||||||
try:
|
try:
|
||||||
template = self.app.jinja2_env.get_template("message.html")
|
template = self.app.jinja2_env.get_template("message.html")
|
||||||
model["html"] = template.render(**context)
|
model["html"] = template.render(**context)
|
||||||
model['html'] = sanitize_html(model['html'])
|
model["html"] = sanitize_html(model["html"])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(ex, flush=True)
|
print(ex, flush=True)
|
||||||
|
|
||||||
@ -99,9 +99,9 @@ class ChannelMessageService(BaseService):
|
|||||||
if not user:
|
if not user:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
#if not message["html"].startswith("<chat-message"):
|
# if not message["html"].startswith("<chat-message"):
|
||||||
#message = await self.get(uid=message["uid"])
|
# message = await self.get(uid=message["uid"])
|
||||||
#await self.save(message)
|
# await self.save(message)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uid": message["uid"],
|
"uid": message["uid"],
|
||||||
@ -129,7 +129,7 @@ class ChannelMessageService(BaseService):
|
|||||||
)
|
)
|
||||||
template = self.app.jinja2_env.get_template("message.html")
|
template = self.app.jinja2_env.get_template("message.html")
|
||||||
model["html"] = template.render(**context)
|
model["html"] = template.render(**context)
|
||||||
model['html'] = sanitize_html(model['html'])
|
model["html"] = sanitize_html(model["html"])
|
||||||
return await super().save(model)
|
return await super().save(model)
|
||||||
|
|
||||||
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
||||||
|
|||||||
@ -37,7 +37,7 @@ class ChatService(BaseService):
|
|||||||
if not channel:
|
if not channel:
|
||||||
raise Exception("Channel not found.")
|
raise Exception("Channel not found.")
|
||||||
channel_message = await self.services.channel_message.get(
|
channel_message = await self.services.channel_message.get(
|
||||||
channel_uid=channel_uid,user_uid=user_uid, is_final=False
|
channel_uid=channel_uid, user_uid=user_uid, is_final=False
|
||||||
)
|
)
|
||||||
if channel_message:
|
if channel_message:
|
||||||
channel_message["message"] = message
|
channel_message["message"] = message
|
||||||
|
|||||||
@ -9,16 +9,18 @@ class ContainerService(BaseService):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.compose_path = "snek-container-compose.yml"
|
self.compose_path = "snek-container-compose.yml"
|
||||||
self.compose = ComposeFileManager(self.compose_path,self.container_event_handler)
|
self.compose = ComposeFileManager(
|
||||||
|
self.compose_path, self.container_event_handler
|
||||||
|
)
|
||||||
self.event_listeners = {}
|
self.event_listeners = {}
|
||||||
|
|
||||||
async def shutdown(self):
|
async def shutdown(self):
|
||||||
return await self.compose.shutdown()
|
return await self.compose.shutdown()
|
||||||
|
|
||||||
async def add_event_listener(self, name, event,event_handler):
|
async def add_event_listener(self, name, event, event_handler):
|
||||||
if not name in self.event_listeners:
|
if name not in self.event_listeners:
|
||||||
self.event_listeners[name] = {}
|
self.event_listeners[name] = {}
|
||||||
if not event in self.event_listeners[name]:
|
if event not in self.event_listeners[name]:
|
||||||
self.event_listeners[name][event] = []
|
self.event_listeners[name][event] = []
|
||||||
self.event_listeners[name][event].append(event_handler)
|
self.event_listeners[name][event].append(event_handler)
|
||||||
|
|
||||||
@ -27,7 +29,7 @@ class ContainerService(BaseService):
|
|||||||
try:
|
try:
|
||||||
self.event_listeners[name][event].remove(event_handler)
|
self.event_listeners[name][event].remove(event_handler)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def container_event_handler(self, name, event, data):
|
async def container_event_handler(self, name, event, data):
|
||||||
event_listeners = self.event_listeners.get(name, {})
|
event_listeners = self.event_listeners.get(name, {})
|
||||||
@ -44,8 +46,10 @@ class ContainerService(BaseService):
|
|||||||
return channel_uid
|
return channel_uid
|
||||||
return f"channel-{channel_uid}"
|
return f"channel-{channel_uid}"
|
||||||
|
|
||||||
async def get(self,channel_uid):
|
async def get(self, channel_uid):
|
||||||
return await self.compose.get_instance(await self.get_container_name(channel_uid))
|
return await self.compose.get_instance(
|
||||||
|
await self.get_container_name(channel_uid)
|
||||||
|
)
|
||||||
|
|
||||||
async def stop(self, channel_uid):
|
async def stop(self, channel_uid):
|
||||||
return await self.compose.stop(await self.get_container_name(channel_uid))
|
return await self.compose.stop(await self.get_container_name(channel_uid))
|
||||||
@ -60,12 +64,15 @@ class ContainerService(BaseService):
|
|||||||
result = await self.create(channel_uid=channel["uid"])
|
result = await self.create(channel_uid=channel["uid"])
|
||||||
print(result)
|
print(result)
|
||||||
|
|
||||||
|
|
||||||
async def get_status(self, channel_uid):
|
async def get_status(self, channel_uid):
|
||||||
return await self.compose.get_instance_status(await self.get_container_name(channel_uid))
|
return await self.compose.get_instance_status(
|
||||||
|
await self.get_container_name(channel_uid)
|
||||||
|
)
|
||||||
|
|
||||||
async def write_stdin(self, channel_uid, data):
|
async def write_stdin(self, channel_uid, data):
|
||||||
return await self.compose.write_stdin(await self.get_container_name(channel_uid), data)
|
return await self.compose.write_stdin(
|
||||||
|
await self.get_container_name(channel_uid), data
|
||||||
|
)
|
||||||
|
|
||||||
async def create(
|
async def create(
|
||||||
self,
|
self,
|
||||||
@ -78,7 +85,7 @@ class ContainerService(BaseService):
|
|||||||
volumes=None,
|
volumes=None,
|
||||||
):
|
):
|
||||||
name = await self.get_container_name(channel_uid)
|
name = await self.get_container_name(channel_uid)
|
||||||
|
|
||||||
test = await self.compose.get_instance(name)
|
test = await self.compose.get_instance(name)
|
||||||
if test:
|
if test:
|
||||||
return test
|
return test
|
||||||
@ -116,4 +123,3 @@ class ContainerService(BaseService):
|
|||||||
if await super().save(model):
|
if await super().save(model):
|
||||||
return model
|
return model
|
||||||
raise Exception(f"Failed to create container: {model.errors}")
|
raise Exception(f"Failed to create container: {model.errors}")
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,25 @@
|
|||||||
# services/forum.py
|
# services/forum.py
|
||||||
from snek.system.service import BaseService
|
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any, Awaitable, Callable, Dict, List
|
from collections.abc import Awaitable
|
||||||
|
from typing import Any, Callable, Dict, List
|
||||||
|
|
||||||
from snek.system.model import now
|
from snek.system.model import now
|
||||||
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
EventListener = Callable[[str, Any], Awaitable[None]] | Callable[[str, Any], None]
|
EventListener = Callable[[str, Any], Awaitable[None]] | Callable[[str, Any], None]
|
||||||
|
|
||||||
|
|
||||||
class BaseForumService(BaseService):
|
class BaseForumService(BaseService):
|
||||||
"""
|
"""
|
||||||
Base mix-in that gives a service `add_notification_listener`,
|
Base mix-in that gives a service `add_notification_listener`,
|
||||||
an internal `_dispatch_event` helper, and a public `notify` method.
|
an internal `_dispatch_event` helper, and a public `notify` method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_timestamp(self):
|
def get_timestamp(self):
|
||||||
return now()
|
return now()
|
||||||
|
|
||||||
def generate_uid(self):
|
def generate_uid(self):
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
@ -64,15 +71,17 @@ class BaseForumService(BaseService):
|
|||||||
class ForumService(BaseForumService):
|
class ForumService(BaseForumService):
|
||||||
mapper_name = "forum"
|
mapper_name = "forum"
|
||||||
|
|
||||||
async def create_forum(self, name, description, created_by_uid, slug=None, icon=None):
|
async def create_forum(
|
||||||
|
self, name, description, created_by_uid, slug=None, icon=None
|
||||||
|
):
|
||||||
if not slug:
|
if not slug:
|
||||||
slug = self.generate_slug(name)
|
slug = self.generate_slug(name)
|
||||||
|
|
||||||
# Check if slug exists
|
# Check if slug exists
|
||||||
existing = await self.get(slug=slug)
|
existing = await self.get(slug=slug)
|
||||||
if existing:
|
if existing:
|
||||||
slug = f"{slug}-{self.generate_uid()[:8]}"
|
slug = f"{slug}-{self.generate_uid()[:8]}"
|
||||||
|
|
||||||
model = await self.new()
|
model = await self.new()
|
||||||
model["name"] = name
|
model["name"] = name
|
||||||
model["description"] = description
|
model["description"] = description
|
||||||
@ -80,7 +89,7 @@ class ForumService(BaseForumService):
|
|||||||
model["created_by_uid"] = created_by_uid
|
model["created_by_uid"] = created_by_uid
|
||||||
if icon:
|
if icon:
|
||||||
model["icon"] = icon
|
model["icon"] = icon
|
||||||
|
|
||||||
if await self.save(model):
|
if await self.save(model):
|
||||||
await self.notify("forum_created", model)
|
await self.notify("forum_created", model)
|
||||||
return model
|
return model
|
||||||
@ -89,8 +98,8 @@ class ForumService(BaseForumService):
|
|||||||
def generate_slug(self, text):
|
def generate_slug(self, text):
|
||||||
# Convert to lowercase and replace spaces with hyphens
|
# Convert to lowercase and replace spaces with hyphens
|
||||||
slug = text.lower().strip()
|
slug = text.lower().strip()
|
||||||
slug = re.sub(r'[^\w\s-]', '', slug)
|
slug = re.sub(r"[^\w\s-]", "", slug)
|
||||||
slug = re.sub(r'[-\s]+', '-', slug)
|
slug = re.sub(r"[-\s]+", "-", slug)
|
||||||
return slug
|
return slug
|
||||||
|
|
||||||
async def get_active_forums(self):
|
async def get_active_forums(self):
|
||||||
@ -105,21 +114,19 @@ class ForumService(BaseForumService):
|
|||||||
await self.save(forum)
|
await self.save(forum)
|
||||||
|
|
||||||
|
|
||||||
# services/thread.py
|
# services/thread.py
|
||||||
class ThreadService(BaseForumService):
|
class ThreadService(BaseForumService):
|
||||||
mapper_name = "thread"
|
mapper_name = "thread"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def create_thread(self, forum_uid, title, content, created_by_uid):
|
async def create_thread(self, forum_uid, title, content, created_by_uid):
|
||||||
# Generate slug
|
# Generate slug
|
||||||
slug = self.services.forum.generate_slug(title)
|
slug = self.services.forum.generate_slug(title)
|
||||||
|
|
||||||
# Check if slug exists in this forum
|
# Check if slug exists in this forum
|
||||||
existing = await self.get(forum_uid=forum_uid, slug=slug)
|
existing = await self.get(forum_uid=forum_uid, slug=slug)
|
||||||
if existing:
|
if existing:
|
||||||
slug = f"{slug}-{self.generate_uid()[:8]}"
|
slug = f"{slug}-{self.generate_uid()[:8]}"
|
||||||
|
|
||||||
# Create thread
|
# Create thread
|
||||||
thread = await self.new()
|
thread = await self.new()
|
||||||
thread["forum_uid"] = forum_uid
|
thread["forum_uid"] = forum_uid
|
||||||
@ -128,7 +135,7 @@ class ThreadService(BaseForumService):
|
|||||||
thread["created_by_uid"] = created_by_uid
|
thread["created_by_uid"] = created_by_uid
|
||||||
thread["last_post_at"] = self.get_timestamp()
|
thread["last_post_at"] = self.get_timestamp()
|
||||||
thread["last_post_by_uid"] = created_by_uid
|
thread["last_post_by_uid"] = created_by_uid
|
||||||
|
|
||||||
if await self.save(thread):
|
if await self.save(thread):
|
||||||
# Create first post
|
# Create first post
|
||||||
post = await self.services.post.create_post(
|
post = await self.services.post.create_post(
|
||||||
@ -136,19 +143,18 @@ class ThreadService(BaseForumService):
|
|||||||
forum_uid=forum_uid,
|
forum_uid=forum_uid,
|
||||||
content=content,
|
content=content,
|
||||||
created_by_uid=created_by_uid,
|
created_by_uid=created_by_uid,
|
||||||
is_first_post=True
|
is_first_post=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update forum counters
|
# Update forum counters
|
||||||
forum = await self.services.forum.get(uid=forum_uid)
|
forum = await self.services.forum.get(uid=forum_uid)
|
||||||
await forum.increment_thread_count()
|
await forum.increment_thread_count()
|
||||||
await self.services.forum.update_last_post(forum_uid, thread["uid"])
|
await self.services.forum.update_last_post(forum_uid, thread["uid"])
|
||||||
|
|
||||||
await self.notify("thread_created", {
|
await self.notify(
|
||||||
"thread": thread,
|
"thread_created", {"thread": thread, "forum_uid": forum_uid}
|
||||||
"forum_uid": forum_uid
|
)
|
||||||
})
|
|
||||||
|
|
||||||
return thread, post
|
return thread, post
|
||||||
raise Exception(f"Failed to create thread: {thread.errors}")
|
raise Exception(f"Failed to create thread: {thread.errors}")
|
||||||
|
|
||||||
@ -156,12 +162,12 @@ class ThreadService(BaseForumService):
|
|||||||
thread = await self.get(uid=thread_uid)
|
thread = await self.get(uid=thread_uid)
|
||||||
if not thread:
|
if not thread:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Check if user is admin
|
# Check if user is admin
|
||||||
user = await self.services.user.get(uid=user_uid)
|
user = await self.services.user.get(uid=user_uid)
|
||||||
if not user.get("is_admin"):
|
if not user.get("is_admin"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
thread["is_pinned"] = not thread["is_pinned"]
|
thread["is_pinned"] = not thread["is_pinned"]
|
||||||
await self.save(thread)
|
await self.save(thread)
|
||||||
return thread
|
return thread
|
||||||
@ -170,12 +176,12 @@ class ThreadService(BaseForumService):
|
|||||||
thread = await self.get(uid=thread_uid)
|
thread = await self.get(uid=thread_uid)
|
||||||
if not thread:
|
if not thread:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Check if user is admin or thread creator
|
# Check if user is admin or thread creator
|
||||||
user = await self.services.user.get(uid=user_uid)
|
user = await self.services.user.get(uid=user_uid)
|
||||||
if not user.get("is_admin") and thread["created_by_uid"] != user_uid:
|
if not user.get("is_admin") and thread["created_by_uid"] != user_uid:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
thread["is_locked"] = not thread["is_locked"]
|
thread["is_locked"] = not thread["is_locked"]
|
||||||
await self.save(thread)
|
await self.save(thread)
|
||||||
return thread
|
return thread
|
||||||
@ -185,19 +191,21 @@ class ThreadService(BaseForumService):
|
|||||||
class PostService(BaseForumService):
|
class PostService(BaseForumService):
|
||||||
mapper_name = "post"
|
mapper_name = "post"
|
||||||
|
|
||||||
async def create_post(self, thread_uid, forum_uid, content, created_by_uid, is_first_post=False):
|
async def create_post(
|
||||||
|
self, thread_uid, forum_uid, content, created_by_uid, is_first_post=False
|
||||||
|
):
|
||||||
# Check if thread is locked
|
# Check if thread is locked
|
||||||
thread = await self.services.thread.get(uid=thread_uid)
|
thread = await self.services.thread.get(uid=thread_uid)
|
||||||
if thread["is_locked"] and not is_first_post:
|
if thread["is_locked"] and not is_first_post:
|
||||||
raise Exception("Thread is locked")
|
raise Exception("Thread is locked")
|
||||||
|
|
||||||
post = await self.new()
|
post = await self.new()
|
||||||
post["thread_uid"] = thread_uid
|
post["thread_uid"] = thread_uid
|
||||||
post["forum_uid"] = forum_uid
|
post["forum_uid"] = forum_uid
|
||||||
post["content"] = content
|
post["content"] = content
|
||||||
post["created_by_uid"] = created_by_uid
|
post["created_by_uid"] = created_by_uid
|
||||||
post["is_first_post"] = is_first_post
|
post["is_first_post"] = is_first_post
|
||||||
|
|
||||||
if await self.save(post):
|
if await self.save(post):
|
||||||
# Update thread counters
|
# Update thread counters
|
||||||
if not is_first_post:
|
if not is_first_post:
|
||||||
@ -205,18 +213,17 @@ class PostService(BaseForumService):
|
|||||||
thread["last_post_at"] = self.get_timestamp()
|
thread["last_post_at"] = self.get_timestamp()
|
||||||
thread["last_post_by_uid"] = created_by_uid
|
thread["last_post_by_uid"] = created_by_uid
|
||||||
await self.services.thread.save(thread)
|
await self.services.thread.save(thread)
|
||||||
|
|
||||||
# Update forum counters
|
# Update forum counters
|
||||||
forum = await self.services.forum.get(uid=forum_uid)
|
forum = await self.services.forum.get(uid=forum_uid)
|
||||||
await forum.increment_post_count()
|
await forum.increment_post_count()
|
||||||
await self.services.forum.update_last_post(forum_uid, thread_uid)
|
await self.services.forum.update_last_post(forum_uid, thread_uid)
|
||||||
|
|
||||||
await self.notify("post_created", {
|
await self.notify(
|
||||||
"post": post,
|
"post_created",
|
||||||
"thread_uid": thread_uid,
|
{"post": post, "thread_uid": thread_uid, "forum_uid": forum_uid},
|
||||||
"forum_uid": forum_uid
|
)
|
||||||
})
|
|
||||||
|
|
||||||
return post
|
return post
|
||||||
raise Exception(f"Failed to create post: {post.errors}")
|
raise Exception(f"Failed to create post: {post.errors}")
|
||||||
|
|
||||||
@ -224,16 +231,16 @@ class PostService(BaseForumService):
|
|||||||
post = await self.get(uid=post_uid)
|
post = await self.get(uid=post_uid)
|
||||||
if not post:
|
if not post:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Check permissions
|
# Check permissions
|
||||||
user = await self.services.user.get(uid=user_uid)
|
user = await self.services.user.get(uid=user_uid)
|
||||||
if post["created_by_uid"] != user_uid and not user.get("is_admin"):
|
if post["created_by_uid"] != user_uid and not user.get("is_admin"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
post["content"] = content
|
post["content"] = content
|
||||||
post["edited_at"] = self.get_timestamp()
|
post["edited_at"] = self.get_timestamp()
|
||||||
post["edited_by_uid"] = user_uid
|
post["edited_by_uid"] = user_uid
|
||||||
|
|
||||||
if await self.save(post):
|
if await self.save(post):
|
||||||
await self.notify("post_edited", post)
|
await self.notify("post_edited", post)
|
||||||
return post
|
return post
|
||||||
@ -243,16 +250,16 @@ class PostService(BaseForumService):
|
|||||||
post = await self.get(uid=post_uid)
|
post = await self.get(uid=post_uid)
|
||||||
if not post:
|
if not post:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check permissions
|
# Check permissions
|
||||||
user = await self.services.user.get(uid=user_uid)
|
user = await self.services.user.get(uid=user_uid)
|
||||||
if post["created_by_uid"] != user_uid and not user.get("is_admin"):
|
if post["created_by_uid"] != user_uid and not user.get("is_admin"):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Don't allow deleting first post
|
# Don't allow deleting first post
|
||||||
if post["is_first_post"]:
|
if post["is_first_post"]:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
post["deleted_at"] = self.get_timestamp()
|
post["deleted_at"] = self.get_timestamp()
|
||||||
if await self.save(post):
|
if await self.save(post):
|
||||||
await self.notify("post_deleted", post)
|
await self.notify("post_deleted", post)
|
||||||
@ -267,38 +274,36 @@ class PostLikeService(BaseForumService):
|
|||||||
async def toggle_like(self, post_uid, user_uid):
|
async def toggle_like(self, post_uid, user_uid):
|
||||||
# Check if already liked
|
# Check if already liked
|
||||||
existing = await self.get(post_uid=post_uid, user_uid=user_uid)
|
existing = await self.get(post_uid=post_uid, user_uid=user_uid)
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
# Unlike
|
# Unlike
|
||||||
await self.delete(uid=existing["uid"])
|
await self.delete(uid=existing["uid"])
|
||||||
|
|
||||||
# Update post like count
|
# Update post like count
|
||||||
post = await self.services.post.get(uid=post_uid)
|
post = await self.services.post.get(uid=post_uid)
|
||||||
post["like_count"] = max(0, post["like_count"] - 1)
|
post["like_count"] = max(0, post["like_count"] - 1)
|
||||||
await self.services.post.save(post)
|
await self.services.post.save(post)
|
||||||
|
|
||||||
await self.notify("post_unliked", {
|
await self.notify(
|
||||||
"post_uid": post_uid,
|
"post_unliked", {"post_uid": post_uid, "user_uid": user_uid}
|
||||||
"user_uid": user_uid
|
)
|
||||||
})
|
|
||||||
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
# Like
|
# Like
|
||||||
like = await self.new()
|
like = await self.new()
|
||||||
like["post_uid"] = post_uid
|
like["post_uid"] = post_uid
|
||||||
like["user_uid"] = user_uid
|
like["user_uid"] = user_uid
|
||||||
|
|
||||||
if await self.save(like):
|
if await self.save(like):
|
||||||
# Update post like count
|
# Update post like count
|
||||||
post = await self.services.post.get(uid=post_uid)
|
post = await self.services.post.get(uid=post_uid)
|
||||||
post["like_count"] += 1
|
post["like_count"] += 1
|
||||||
await self.services.post.save(post)
|
await self.services.post.save(post)
|
||||||
|
|
||||||
await self.notify("post_liked", {
|
await self.notify(
|
||||||
"post_uid": post_uid,
|
"post_liked", {"post_uid": post_uid, "user_uid": user_uid}
|
||||||
"user_uid": user_uid
|
)
|
||||||
})
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
from snek.system.service import BaseService
|
import sqlite3
|
||||||
import sqlite3
|
|
||||||
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
|
||||||
class StatisticsService(BaseService):
|
class StatisticsService(BaseService):
|
||||||
|
|
||||||
def database(self):
|
def database(self):
|
||||||
db_path = self.app.db_path.split("///")[-1]
|
db_path = self.app.db_path.split("///")[-1]
|
||||||
print(db_path)
|
print(db_path)
|
||||||
@ -15,9 +17,20 @@ class StatisticsService(BaseService):
|
|||||||
return cursor.fetchall()
|
return cursor.fetchall()
|
||||||
|
|
||||||
tables = [
|
tables = [
|
||||||
'http_access', 'user', 'channel', 'channel_member', 'broadcast',
|
"http_access",
|
||||||
'channel_message', 'notification', 'repository', 'test', 'drive',
|
"user",
|
||||||
'user_property', 'a', 'channel_attachment', 'push_registration'
|
"channel",
|
||||||
|
"channel_member",
|
||||||
|
"broadcast",
|
||||||
|
"channel_message",
|
||||||
|
"notification",
|
||||||
|
"repository",
|
||||||
|
"test",
|
||||||
|
"drive",
|
||||||
|
"user_property",
|
||||||
|
"a",
|
||||||
|
"channel_attachment",
|
||||||
|
"push_registration",
|
||||||
]
|
]
|
||||||
|
|
||||||
for table in tables:
|
for table in tables:
|
||||||
@ -36,30 +49,50 @@ class StatisticsService(BaseService):
|
|||||||
print(f"\nColumn: {name} ({col_type})")
|
print(f"\nColumn: {name} ({col_type})")
|
||||||
print(f"Distinct values: {distinct_count}")
|
print(f"Distinct values: {distinct_count}")
|
||||||
|
|
||||||
if 'INT' in col_type_upper or 'BIGINT' in col_type_upper or 'FLOAT' in col_type_upper:
|
if (
|
||||||
cursor.execute(f"SELECT MIN('{name}'), MAX('{name}'), AVG('{name}') FROM {table} WHERE '{name}' IS NOT NULL")
|
"INT" in col_type_upper
|
||||||
|
or "BIGINT" in col_type_upper
|
||||||
|
or "FLOAT" in col_type_upper
|
||||||
|
):
|
||||||
|
cursor.execute(
|
||||||
|
f"SELECT MIN('{name}'), MAX('{name}'), AVG('{name}') FROM {table} WHERE '{name}' IS NOT NULL"
|
||||||
|
)
|
||||||
min_val, max_val, avg_val = cursor.fetchone()
|
min_val, max_val, avg_val = cursor.fetchone()
|
||||||
print(f"Min: {min_val}, Max: {max_val}, Avg: {avg_val}")
|
print(f"Min: {min_val}, Max: {max_val}, Avg: {avg_val}")
|
||||||
|
|
||||||
elif 'TEXT' in col_type_upper and ('date' in name.lower() or 'time' in name.lower() or 'created' in name.lower() or 'updated' in name.lower() or 'on' in name.lower()):
|
elif "TEXT" in col_type_upper and (
|
||||||
cursor.execute(f"SELECT MIN({name}), MAX({name}) FROM {table} WHERE {name} IS NOT NULL")
|
"date" in name.lower()
|
||||||
|
or "time" in name.lower()
|
||||||
|
or "created" in name.lower()
|
||||||
|
or "updated" in name.lower()
|
||||||
|
or "on" in name.lower()
|
||||||
|
):
|
||||||
|
cursor.execute(
|
||||||
|
f"SELECT MIN({name}), MAX({name}) FROM {table} WHERE {name} IS NOT NULL"
|
||||||
|
)
|
||||||
min_date, max_date = cursor.fetchone()
|
min_date, max_date = cursor.fetchone()
|
||||||
print(f"Earliest: {min_date}, Latest: {max_date}")
|
print(f"Earliest: {min_date}, Latest: {max_date}")
|
||||||
|
|
||||||
elif 'TEXT' in col_type_upper:
|
elif "TEXT" in col_type_upper:
|
||||||
cursor.execute(f"SELECT LENGTH({name}) FROM {table} WHERE {name} IS NOT NULL")
|
cursor.execute(
|
||||||
|
f"SELECT LENGTH({name}) FROM {table} WHERE {name} IS NOT NULL"
|
||||||
|
)
|
||||||
lengths = [len_row[0] for len_row in cursor.fetchall()]
|
lengths = [len_row[0] for len_row in cursor.fetchall()]
|
||||||
if lengths:
|
if lengths:
|
||||||
avg_length = sum(lengths) / len(lengths)
|
avg_length = sum(lengths) / len(lengths)
|
||||||
max_length = max(lengths)
|
max_length = max(lengths)
|
||||||
min_length = min(lengths)
|
min_length = min(lengths)
|
||||||
print(f"Avg length: {avg_length:.2f}, Max length: {max_length}, Min length: {min_length}")
|
print(
|
||||||
|
f"Avg length: {avg_length:.2f}, Max length: {max_length}, Min length: {min_length}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print("No data to compute length statistics.")
|
print("No data to compute length statistics.")
|
||||||
|
|
||||||
# New statistics functions
|
# New statistics functions
|
||||||
def get_time_series_stats(table_name, date_column):
|
def get_time_series_stats(table_name, date_column):
|
||||||
cursor.execute(f"SELECT strftime('%Y-%m-%d', {date_column}) AS day, COUNT(*) FROM {table_name} GROUP BY day")
|
cursor.execute(
|
||||||
|
f"SELECT strftime('%Y-%m-%d', {date_column}) AS day, COUNT(*) FROM {table_name} GROUP BY day"
|
||||||
|
)
|
||||||
return cursor.fetchall()
|
return cursor.fetchall()
|
||||||
|
|
||||||
def get_count_created(table_name, date_column):
|
def get_count_created(table_name, date_column):
|
||||||
@ -67,17 +100,21 @@ class StatisticsService(BaseService):
|
|||||||
return cursor.fetchone()[0]
|
return cursor.fetchone()[0]
|
||||||
|
|
||||||
def get_channels_per_user():
|
def get_channels_per_user():
|
||||||
cursor.execute("SELECT user_uid, COUNT(*) AS channel_count FROM channel_member GROUP BY user_uid ORDER BY channel_count DESC")
|
cursor.execute(
|
||||||
|
"SELECT user_uid, COUNT(*) AS channel_count FROM channel_member GROUP BY user_uid ORDER BY channel_count DESC"
|
||||||
|
)
|
||||||
return cursor.fetchall()
|
return cursor.fetchall()
|
||||||
|
|
||||||
def get_online_users():
|
def get_online_users():
|
||||||
cursor.execute("SELECT COUNT(*) FROM user WHERE last_ping >= datetime('now', '-5 minutes')")
|
cursor.execute(
|
||||||
|
"SELECT COUNT(*) FROM user WHERE last_ping >= datetime('now', '-5 minutes')"
|
||||||
|
)
|
||||||
return cursor.fetchone()[0]
|
return cursor.fetchone()[0]
|
||||||
|
|
||||||
# Example usage of new functions
|
# Example usage of new functions
|
||||||
messages_per_day = get_time_series_stats('channel_message', 'created_at')
|
messages_per_day = get_time_series_stats("channel_message", "created_at")
|
||||||
users_created = get_count_created('user', 'created_at')
|
users_created = get_count_created("user", "created_at")
|
||||||
channels_created = get_count_created('channel', 'created_at')
|
channels_created = get_count_created("channel", "created_at")
|
||||||
channels_per_user = get_channels_per_user()
|
channels_per_user = get_channels_per_user()
|
||||||
online_users = get_online_users()
|
online_users = get_online_users()
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class UserPropertyService(BaseService):
|
|||||||
|
|
||||||
async def set(self, user_uid, name, value):
|
async def set(self, user_uid, name, value):
|
||||||
self.mapper.db.upsert(
|
self.mapper.db.upsert(
|
||||||
"user_property",
|
"user_property",
|
||||||
{
|
{
|
||||||
"user_uid": user_uid,
|
"user_uid": user_uid,
|
||||||
"name": name,
|
"name": name,
|
||||||
|
|||||||
@ -1,19 +1,16 @@
|
|||||||
from snek.app import Application
|
|
||||||
from IPython import start_ipython
|
from IPython import start_ipython
|
||||||
|
|
||||||
|
from snek.app import Application
|
||||||
|
|
||||||
|
|
||||||
class Shell:
|
class Shell:
|
||||||
def __init__(self,db_path):
|
def __init__(self, db_path):
|
||||||
self.app = Application(db_path=f"sqlite:///{db_path}")
|
self.app = Application(db_path=f"sqlite:///{db_path}")
|
||||||
|
|
||||||
async def maintenance(self):
|
async def maintenance(self):
|
||||||
await self.app.services.container.maintenance()
|
await self.app.services.container.maintenance()
|
||||||
await self.app.services.channel_message.maintenance()
|
await self.app.services.channel_message.maintenance()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
ns = {
|
ns = {"app": self.app, "maintenance": self.maintenance}
|
||||||
"app": self.app,
|
|
||||||
"maintenance": self.maintenance
|
|
||||||
}
|
|
||||||
start_ipython(argv=[], user_ns=ns)
|
start_ipython(argv=[], user_ns=ns)
|
||||||
|
|||||||
@ -1,28 +1,27 @@
|
|||||||
import re
|
import asyncio
|
||||||
|
import atexit
|
||||||
import json
|
import json
|
||||||
from uuid import uuid4
|
import os
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import unittest
|
||||||
|
from collections.abc import AsyncGenerator, Iterable
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Dict,
|
Dict,
|
||||||
Iterable,
|
|
||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
AsyncGenerator,
|
|
||||||
Union,
|
|
||||||
Tuple,
|
|
||||||
Set,
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
)
|
)
|
||||||
from pathlib import Path
|
from uuid import uuid4
|
||||||
import aiosqlite
|
|
||||||
import unittest
|
|
||||||
from types import SimpleNamespace
|
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
import aiosqlite
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import socket
|
|
||||||
import os
|
|
||||||
import atexit
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncDataSet:
|
class AsyncDataSet:
|
||||||
@ -103,7 +102,7 @@ class AsyncDataSet:
|
|||||||
# No socket file, become server
|
# No socket file, become server
|
||||||
self._is_server = True
|
self._is_server = True
|
||||||
await self._setup_server()
|
await self._setup_server()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
# Fallback to client mode
|
# Fallback to client mode
|
||||||
self._is_server = False
|
self._is_server = False
|
||||||
await self._setup_client()
|
await self._setup_client()
|
||||||
@ -213,7 +212,7 @@ class AsyncDataSet:
|
|||||||
data = await request.json()
|
data = await request.json()
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
_limit = data.pop("_limit",60)
|
_limit = data.pop("_limit", 60)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -751,7 +750,7 @@ class AsyncDataSet:
|
|||||||
for k, v in where.items()
|
for k, v in where.items()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return " WHERE " + " AND ".join(clauses), list(v for v in vals if v is not None)
|
return " WHERE " + " AND ".join(clauses), [v for v in vals if v is not None]
|
||||||
|
|
||||||
async def _server_insert(
|
async def _server_insert(
|
||||||
self, table: str, args: Dict[str, Any], return_id: bool = False
|
self, table: str, args: Dict[str, Any], return_id: bool = False
|
||||||
@ -887,7 +886,6 @@ class AsyncDataSet:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
where_clause, where_params = self._build_where(where)
|
where_clause, where_params = self._build_where(where)
|
||||||
order_clause = f" ORDER BY {order_by}" if order_by else ""
|
order_clause = f" ORDER BY {order_by}" if order_by else ""
|
||||||
extra = (f" LIMIT {_limit}" if _limit else "") + (
|
extra = (f" LIMIT {_limit}" if _limit else "") + (
|
||||||
@ -1209,4 +1207,3 @@ class TestAsyncDataSet(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import copy
|
|
||||||
import json
|
|
||||||
import yaml
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import subprocess
|
import copy
|
||||||
|
import json
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pty
|
import pty
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@ -10,26 +11,28 @@ except Exception as ex:
|
|||||||
print(ex)
|
print(ex)
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
class ComposeFileManager:
|
class ComposeFileManager:
|
||||||
def __init__(self, compose_path="docker-compose.yml",event_handler=None):
|
def __init__(self, compose_path="docker-compose.yml", event_handler=None):
|
||||||
self.compose_path = compose_path
|
self.compose_path = compose_path
|
||||||
self._load()
|
self._load()
|
||||||
self.running_instances = {}
|
self.running_instances = {}
|
||||||
self.event_handler = event_handler
|
self.event_handler = event_handler
|
||||||
|
|
||||||
async def shutdown(self):
|
async def shutdown(self):
|
||||||
print("Stopping all sessions")
|
print("Stopping all sessions")
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
for name in self.list_instances():
|
for name in self.list_instances():
|
||||||
proc = self.running_instances.get(name)
|
proc = self.running_instances.get(name)
|
||||||
if not proc:
|
if not proc:
|
||||||
continue
|
continue
|
||||||
if proc['proc'].returncode == None:
|
if proc["proc"].returncode is None:
|
||||||
print("Stopping",name)
|
print("Stopping", name)
|
||||||
tasks.append(asyncio.create_task(proc['proc'].stop()))
|
tasks.append(asyncio.create_task(proc["proc"].stop()))
|
||||||
print("Stopped",name,"gracefully")
|
print("Stopped", name, "gracefully")
|
||||||
return tasks
|
return tasks
|
||||||
|
|
||||||
def _load(self):
|
def _load(self):
|
||||||
try:
|
try:
|
||||||
with open(self.compose_path) as f:
|
with open(self.compose_path) as f:
|
||||||
@ -47,22 +50,21 @@ class ComposeFileManager:
|
|||||||
async def _create_readers(self, container_name):
|
async def _create_readers(self, container_name):
|
||||||
instance = await self.get_instance(container_name)
|
instance = await self.get_instance(container_name)
|
||||||
if not instance:
|
if not instance:
|
||||||
return False
|
return False
|
||||||
proc = self.running_instances.get(container_name)
|
proc = self.running_instances.get(container_name)
|
||||||
if not proc:
|
if not proc:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def reader(event_handler,stream):
|
async def reader(event_handler, stream):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
while True:
|
while True:
|
||||||
line = await loop.run_in_executor(None,os.read,stream,1024)
|
line = await loop.run_in_executor(None, os.read, stream, 1024)
|
||||||
if not line:
|
if not line:
|
||||||
break
|
break
|
||||||
await event_handler(container_name,"stdout",line)
|
await event_handler(container_name, "stdout", line)
|
||||||
await self.stop(container_name)
|
await self.stop(container_name)
|
||||||
asyncio.create_task(reader(self.event_handler,proc['master']))
|
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.create_task(reader(self.event_handler, proc["master"]))
|
||||||
|
|
||||||
def create_instance(
|
def create_instance(
|
||||||
self,
|
self,
|
||||||
@ -79,7 +81,7 @@ class ComposeFileManager:
|
|||||||
"context": ".",
|
"context": ".",
|
||||||
"dockerfile": "DockerfileUbuntu",
|
"dockerfile": "DockerfileUbuntu",
|
||||||
},
|
},
|
||||||
"user":"root",
|
"user": "root",
|
||||||
"working_dir": "/home/retoor/projects/snek",
|
"working_dir": "/home/retoor/projects/snek",
|
||||||
"environment": [f"SNEK_UID={name}"],
|
"environment": [f"SNEK_UID={name}"],
|
||||||
}
|
}
|
||||||
@ -109,9 +111,9 @@ class ComposeFileManager:
|
|||||||
instance = self.compose.get("services", {}).get(name)
|
instance = self.compose.get("services", {}).get(name)
|
||||||
if not instance:
|
if not instance:
|
||||||
return None
|
return None
|
||||||
instance = json.loads(json.dumps(instance,default=str))
|
instance = json.loads(json.dumps(instance, default=str))
|
||||||
instance['status'] = await self.get_instance_status(name)
|
instance["status"] = await self.get_instance_status(name)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def duplicate_instance(self, name, new_name):
|
def duplicate_instance(self, name, new_name):
|
||||||
orig = self.get_instance(name)
|
orig = self.get_instance(name)
|
||||||
@ -135,7 +137,14 @@ class ComposeFileManager:
|
|||||||
if name not in self.list_instances():
|
if name not in self.list_instances():
|
||||||
return "error"
|
return "error"
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
"docker", "compose", "-f", self.compose_path, "ps", "--services", "--filter", f"status=running",
|
"docker",
|
||||||
|
"compose",
|
||||||
|
"-f",
|
||||||
|
self.compose_path,
|
||||||
|
"ps",
|
||||||
|
"--services",
|
||||||
|
"--filter",
|
||||||
|
f"status=running",
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
)
|
)
|
||||||
@ -148,25 +157,30 @@ class ComposeFileManager:
|
|||||||
await self.event_handler(name, "stdin", data)
|
await self.event_handler(name, "stdin", data)
|
||||||
proc = self.running_instances.get(name)
|
proc = self.running_instances.get(name)
|
||||||
if not proc:
|
if not proc:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
os.write(proc['master'], data.encode())
|
os.write(proc["master"], data.encode())
|
||||||
return True
|
return True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(ex)
|
print(ex)
|
||||||
await self.stop(name)
|
await self.stop(name)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def stop(self, name):
|
async def stop(self, name):
|
||||||
"""Asynchronously stop a container by doing 'docker compose stop [name]'."""
|
"""Asynchronously stop a container by doing 'docker compose stop [name]'."""
|
||||||
if name not in self.list_instances():
|
if name not in self.list_instances():
|
||||||
return False
|
return False
|
||||||
status = await self.get_instance_status(name)
|
status = await self.get_instance_status(name)
|
||||||
if status != "running":
|
if status != "running":
|
||||||
return True
|
return True
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
"docker", "compose", "-f", self.compose_path, "stop", name,
|
"docker",
|
||||||
|
"compose",
|
||||||
|
"-f",
|
||||||
|
self.compose_path,
|
||||||
|
"stop",
|
||||||
|
name,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
)
|
)
|
||||||
@ -177,30 +191,41 @@ class ComposeFileManager:
|
|||||||
try:
|
try:
|
||||||
stdout, stderr = await proc.communicate()
|
stdout, stderr = await proc.communicate()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
stdout = b''
|
stdout = b""
|
||||||
stderr = str(ex).encode()
|
stderr = str(ex).encode()
|
||||||
await self.event_handler(name,"stdout",stdout or stderr)
|
await self.event_handler(name, "stdout", stdout or stderr)
|
||||||
print("Return code", proc.returncode)
|
print("Return code", proc.returncode)
|
||||||
if stdout:
|
if stdout:
|
||||||
await self.event_handler(name,"stdout",stdout or b'')
|
await self.event_handler(name, "stdout", stdout or b"")
|
||||||
return stdout and stdout.decode(errors="ignore") or ""
|
return stdout and stdout.decode(errors="ignore") or ""
|
||||||
|
|
||||||
await self.event_handler(name,"stdout",stderr or b'')
|
await self.event_handler(name, "stdout", stderr or b"")
|
||||||
return stderr and stderr.decode(errors="ignore") or ""
|
return stderr and stderr.decode(errors="ignore") or ""
|
||||||
|
|
||||||
async def start(self, name):
|
async def start(self, name):
|
||||||
"""Asynchronously start a container by doing 'docker compose up -d [name]'."""
|
"""Asynchronously start a container by doing 'docker compose up -d [name]'."""
|
||||||
if name not in self.list_instances():
|
if name not in self.list_instances():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
status = await self.get_instance_status(name)
|
status = await self.get_instance_status(name)
|
||||||
if name in self.running_instances and status == "running" and self.running_instances.get(name) and self.running_instances.get(name).get('proc').returncode == None:
|
if (
|
||||||
return True
|
name in self.running_instances
|
||||||
|
and status == "running"
|
||||||
|
and self.running_instances.get(name)
|
||||||
|
and self.running_instances.get(name).get("proc").returncode is None
|
||||||
|
):
|
||||||
|
return True
|
||||||
elif name in self.running_instances:
|
elif name in self.running_instances:
|
||||||
del self.running_instances[name]
|
del self.running_instances[name]
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
"docker", "compose", "-f", self.compose_path, "up", name, "-d",
|
"docker",
|
||||||
|
"compose",
|
||||||
|
"-f",
|
||||||
|
self.compose_path,
|
||||||
|
"up",
|
||||||
|
name,
|
||||||
|
"-d",
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
stdin=asyncio.subprocess.PIPE,
|
stdin=asyncio.subprocess.PIPE,
|
||||||
@ -210,24 +235,29 @@ class ComposeFileManager:
|
|||||||
try:
|
try:
|
||||||
stdout, stderr = await proc.communicate()
|
stdout, stderr = await proc.communicate()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
stdout = b''
|
stdout = b""
|
||||||
stderr = str(ex).encode()
|
stderr = str(ex).encode()
|
||||||
if stdout:
|
if stdout:
|
||||||
print(stdout.decode(errors="ignore"))
|
print(stdout.decode(errors="ignore"))
|
||||||
if stderr:
|
if stderr:
|
||||||
print(stderr.decode(errors="ignore"))
|
print(stderr.decode(errors="ignore"))
|
||||||
await self.event_handler(name,"stdout",stdout or stderr)
|
await self.event_handler(name, "stdout", stdout or stderr)
|
||||||
print("Return code", proc.returncode)
|
print("Return code", proc.returncode)
|
||||||
|
|
||||||
master, slave = pty.openpty()
|
master, slave = pty.openpty()
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
"docker", "compose", "-f", self.compose_path, "exec", name, "/usr/local/bin/entry",
|
"docker",
|
||||||
|
"compose",
|
||||||
|
"-f",
|
||||||
|
self.compose_path,
|
||||||
|
"exec",
|
||||||
|
name,
|
||||||
|
"/usr/local/bin/entry",
|
||||||
stdin=slave,
|
stdin=slave,
|
||||||
stdout=slave,
|
stdout=slave,
|
||||||
stderr=slave,
|
stderr=slave,
|
||||||
)
|
)
|
||||||
proc = {'proc': proc, 'master': master, 'slave': slave}
|
proc = {"proc": proc, "master": master, "slave": slave}
|
||||||
self.running_instances[name] = proc
|
self.running_instances[name] = proc
|
||||||
await self._create_readers(name)
|
await self._create_readers(name)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
DEFAULT_LIMIT = 30
|
DEFAULT_LIMIT = 30
|
||||||
import asyncio
|
import asyncio
|
||||||
import typing
|
import typing
|
||||||
import traceback
|
|
||||||
from snek.system.model import BaseModel
|
from snek.system.model import BaseModel
|
||||||
|
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ class BaseMapper:
|
|||||||
default_limit: int = DEFAULT_LIMIT
|
default_limit: int = DEFAULT_LIMIT
|
||||||
table_name: str = None
|
table_name: str = None
|
||||||
semaphore = asyncio.Semaphore(1)
|
semaphore = asyncio.Semaphore(1)
|
||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
@ -27,17 +27,16 @@ class BaseMapper:
|
|||||||
|
|
||||||
async def run_in_executor(self, func, *args, **kwargs):
|
async def run_in_executor(self, func, *args, **kwargs):
|
||||||
use_semaphore = kwargs.pop("use_semaphore", False)
|
use_semaphore = kwargs.pop("use_semaphore", False)
|
||||||
|
|
||||||
def _execute():
|
def _execute():
|
||||||
result = func(*args, **kwargs)
|
result = func(*args, **kwargs)
|
||||||
if use_semaphore:
|
if use_semaphore:
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
return _execute()
|
|
||||||
#async with self.semaphore:
|
|
||||||
# return await self.loop.run_in_executor(None, _execute)
|
|
||||||
|
|
||||||
|
return _execute()
|
||||||
|
# async with self.semaphore:
|
||||||
|
# return await self.loop.run_in_executor(None, _execute)
|
||||||
|
|
||||||
async def new(self):
|
async def new(self):
|
||||||
return self.model_class(mapper=self, app=self.app)
|
return self.model_class(mapper=self, app=self.app)
|
||||||
@ -51,7 +50,7 @@ class BaseMapper:
|
|||||||
kwargs["uid"] = uid
|
kwargs["uid"] = uid
|
||||||
if not kwargs.get("deleted_at"):
|
if not kwargs.get("deleted_at"):
|
||||||
kwargs["deleted_at"] = None
|
kwargs["deleted_at"] = None
|
||||||
#traceback.print_exc()
|
# traceback.print_exc()
|
||||||
|
|
||||||
record = await self.db.get(self.table_name, kwargs)
|
record = await self.db.get(self.table_name, kwargs)
|
||||||
if not record:
|
if not record:
|
||||||
@ -65,20 +64,18 @@ class BaseMapper:
|
|||||||
async def exists(self, **kwargs):
|
async def exists(self, **kwargs):
|
||||||
return await self.db.count(self.table_name, kwargs)
|
return await self.db.count(self.table_name, kwargs)
|
||||||
|
|
||||||
|
# return await self.run_in_executor(self.table.exists, **kwargs)
|
||||||
#return await self.run_in_executor(self.table.exists, **kwargs)
|
|
||||||
|
|
||||||
async def count(self, **kwargs) -> int:
|
async def count(self, **kwargs) -> int:
|
||||||
return await self.db.count(self.table_name,kwargs)
|
return await self.db.count(self.table_name, kwargs)
|
||||||
|
|
||||||
|
|
||||||
async def save(self, model: BaseModel) -> bool:
|
async def save(self, model: BaseModel) -> bool:
|
||||||
if not model.record.get("uid"):
|
if not model.record.get("uid"):
|
||||||
raise Exception(f"Attempt to save without uid: {model.record}.")
|
raise Exception(f"Attempt to save without uid: {model.record}.")
|
||||||
model.updated_at.update()
|
model.updated_at.update()
|
||||||
await self.upsert(model)
|
await self.upsert(model)
|
||||||
return model
|
return model
|
||||||
#return await self.run_in_executor(self.table.upsert, model.record, ["uid"],use_semaphore=True)
|
# return await self.run_in_executor(self.table.upsert, model.record, ["uid"],use_semaphore=True)
|
||||||
|
|
||||||
async def find(self, **kwargs) -> typing.AsyncGenerator:
|
async def find(self, **kwargs) -> typing.AsyncGenerator:
|
||||||
if not kwargs.get("_limit"):
|
if not kwargs.get("_limit"):
|
||||||
@ -100,10 +97,12 @@ class BaseMapper:
|
|||||||
yield dict(record)
|
yield dict(record)
|
||||||
|
|
||||||
async def update(self, model):
|
async def update(self, model):
|
||||||
if not model["deleted_at"] is None:
|
if model["deleted_at"] is not None:
|
||||||
raise Exception("Can't update deleted record.")
|
raise Exception("Can't update deleted record.")
|
||||||
model.updated_at.update()
|
model.updated_at.update()
|
||||||
return await self.db.update(self.table_name, model.record, {"uid": model["uid"]})
|
return await self.db.update(
|
||||||
|
self.table_name, model.record, {"uid": model["uid"]}
|
||||||
|
)
|
||||||
|
|
||||||
async def upsert(self, model):
|
async def upsert(self, model):
|
||||||
model.updated_at.update()
|
model.updated_at.update()
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import re
|
import re
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from app.cache import time_cache_async, time_cache
|
from app.cache import time_cache, time_cache_async
|
||||||
from mistune import HTMLRenderer, Markdown
|
from mistune import HTMLRenderer, Markdown
|
||||||
from mistune.plugins.formatting import strikethrough
|
from mistune.plugins.formatting import strikethrough
|
||||||
from mistune.plugins.spoiler import spoiler
|
from mistune.plugins.spoiler import spoiler
|
||||||
@ -12,7 +12,6 @@ from pygments.formatters import html
|
|||||||
from pygments.lexers import get_lexer_by_name
|
from pygments.lexers import get_lexer_by_name
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def strip_markdown(md_text):
|
def strip_markdown(md_text):
|
||||||
# Remove code blocks (
|
# Remove code blocks (
|
||||||
md_text = re.sub(r"[\s\S]?```", "", md_text)
|
md_text = re.sub(r"[\s\S]?```", "", md_text)
|
||||||
@ -50,7 +49,7 @@ class MarkdownRenderer(HTMLRenderer):
|
|||||||
return get_lexer_by_name(lang, stripall=True)
|
return get_lexer_by_name(lang, stripall=True)
|
||||||
except:
|
except:
|
||||||
return get_lexer_by_name(default, stripall=True)
|
return get_lexer_by_name(default, stripall=True)
|
||||||
|
|
||||||
@time_cache(timeout=60 * 60)
|
@time_cache(timeout=60 * 60)
|
||||||
def block_code(self, code, lang=None, info=None):
|
def block_code(self, code, lang=None, info=None):
|
||||||
if not lang:
|
if not lang:
|
||||||
|
|||||||
@ -3,31 +3,17 @@
|
|||||||
# This code provides middleware functions for an aiohttp server to manage and modify CSP, CORS, and authentication headers.
|
# This code provides middleware functions for an aiohttp server to manage and modify CSP, CORS, and authentication headers.
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def csp_middleware(request, handler):
|
async def csp_middleware(request, handler):
|
||||||
nonce = secrets.token_hex(16)
|
nonce = secrets.token_hex(16)
|
||||||
origin = request.headers.get('Origin')
|
request.headers.get("Origin")
|
||||||
csp_policy = (
|
request["csp_nonce"] = nonce
|
||||||
"default-src 'self'; "
|
|
||||||
f"script-src 'self' {origin} 'nonce-{nonce}'; "
|
|
||||||
f"style-src 'self' 'unsafe-inline' {origin} 'nonce-{nonce}'; "
|
|
||||||
"img-src *; "
|
|
||||||
"connect-src 'self' https://umami.molodetz.nl; "
|
|
||||||
"font-src *; "
|
|
||||||
"object-src 'none'; "
|
|
||||||
"base-uri 'self'; "
|
|
||||||
"form-action 'self'; "
|
|
||||||
"frame-src 'self'; "
|
|
||||||
"worker-src *; "
|
|
||||||
"media-src *; "
|
|
||||||
"manifest-src 'self';"
|
|
||||||
)
|
|
||||||
request['csp_nonce'] = nonce
|
|
||||||
response = await handler(request)
|
response = await handler(request)
|
||||||
#response.headers['Content-Security-Policy'] = csp_policy
|
# response.headers['Content-Security-Policy'] = csp_policy
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +23,7 @@ async def no_cors_middleware(request, handler):
|
|||||||
response.headers.pop("Access-Control-Allow-Origin", None)
|
response.headers.pop("Access-Control-Allow-Origin", None)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def cors_allow_middleware(request, handler):
|
async def cors_allow_middleware(request, handler):
|
||||||
response = await handler(request)
|
response = await handler(request)
|
||||||
@ -48,6 +35,7 @@ async def cors_allow_middleware(request, handler):
|
|||||||
response.headers["Access-Control-Allow-Credentials"] = "true"
|
response.headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def auth_middleware(request, handler):
|
async def auth_middleware(request, handler):
|
||||||
request["user"] = None
|
request["user"] = None
|
||||||
@ -57,6 +45,7 @@ async def auth_middleware(request, handler):
|
|||||||
)
|
)
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def cors_middleware(request, handler):
|
async def cors_middleware(request, handler):
|
||||||
if request.headers.get("Allow"):
|
if request.headers.get("Allow"):
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from snek.mapper import get_mapper
|
from snek.mapper import get_mapper
|
||||||
from snek.model.user import UserModel
|
|
||||||
from snek.system.mapper import BaseMapper
|
from snek.system.mapper import BaseMapper
|
||||||
|
|
||||||
|
|
||||||
@ -40,7 +39,7 @@ class BaseService:
|
|||||||
yield record
|
yield record
|
||||||
|
|
||||||
async def get(self, *args, **kwargs):
|
async def get(self, *args, **kwargs):
|
||||||
if not "deleted_at" in kwargs:
|
if "deleted_at" not in kwargs:
|
||||||
kwargs["deleted_at"] = None
|
kwargs["deleted_at"] = None
|
||||||
uid = kwargs.get("uid")
|
uid = kwargs.get("uid")
|
||||||
if args:
|
if args:
|
||||||
@ -50,7 +49,7 @@ class BaseService:
|
|||||||
if result and result.__class__ == self.mapper.model_class:
|
if result and result.__class__ == self.mapper.model_class:
|
||||||
return result
|
return result
|
||||||
kwargs["uid"] = uid
|
kwargs["uid"] = uid
|
||||||
print(kwargs,"ZZZZZZZ")
|
print(kwargs, "ZZZZZZZ")
|
||||||
result = await self.mapper.get(**kwargs)
|
result = await self.mapper.get(**kwargs)
|
||||||
if result:
|
if result:
|
||||||
await self.cache.set(result["uid"], result)
|
await self.cache.set(result["uid"], result)
|
||||||
|
|||||||
@ -1,34 +1,41 @@
|
|||||||
import asyncio
|
|
||||||
from aiohttp import web, WSMsgType
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
from collections import defaultdict
|
|
||||||
import html
|
import html
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
from aiohttp import WSMsgType, web
|
||||||
|
|
||||||
|
|
||||||
def create_stats_structure():
|
def create_stats_structure():
|
||||||
"""Creates the nested dictionary structure for storing statistics."""
|
"""Creates the nested dictionary structure for storing statistics."""
|
||||||
|
|
||||||
def nested_dd():
|
def nested_dd():
|
||||||
return defaultdict(lambda: defaultdict(int))
|
return defaultdict(lambda: defaultdict(int))
|
||||||
|
|
||||||
return defaultdict(nested_dd)
|
return defaultdict(nested_dd)
|
||||||
|
|
||||||
|
|
||||||
def get_time_keys(dt: datetime):
|
def get_time_keys(dt: datetime):
|
||||||
"""Generates dictionary keys for different time granularities."""
|
"""Generates dictionary keys for different time granularities."""
|
||||||
return {
|
return {
|
||||||
"hour": dt.strftime('%Y-%m-%d-%H'),
|
"hour": dt.strftime("%Y-%m-%d-%H"),
|
||||||
"day": dt.strftime('%Y-%m-%d'),
|
"day": dt.strftime("%Y-%m-%d"),
|
||||||
"week": dt.strftime('%Y-%W'), # Week number, Monday is first day
|
"week": dt.strftime("%Y-%W"), # Week number, Monday is first day
|
||||||
"month": dt.strftime('%Y-%m'),
|
"month": dt.strftime("%Y-%m"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def update_stats_counters(stats_dict: defaultdict, now: datetime):
|
def update_stats_counters(stats_dict: defaultdict, now: datetime):
|
||||||
"""Increments the appropriate time-based counters in a stats dictionary."""
|
"""Increments the appropriate time-based counters in a stats dictionary."""
|
||||||
keys = get_time_keys(now)
|
keys = get_time_keys(now)
|
||||||
stats_dict['by_hour'][keys['hour']] += 1
|
stats_dict["by_hour"][keys["hour"]] += 1
|
||||||
stats_dict['by_day'][keys['day']] += 1
|
stats_dict["by_day"][keys["day"]] += 1
|
||||||
stats_dict['by_week'][keys['week']] += 1
|
stats_dict["by_week"][keys["week"]] += 1
|
||||||
stats_dict['by_month'][keys['month']] += 1
|
stats_dict["by_month"][keys["month"]] += 1
|
||||||
|
|
||||||
def generate_time_series_svg(title: str, data: list[tuple[str, int]], y_label: str) -> str:
|
|
||||||
|
def generate_time_series_svg(
|
||||||
|
title: str, data: list[tuple[str, int]], y_label: str
|
||||||
|
) -> str:
|
||||||
"""Generates a responsive SVG bar chart for time-series data."""
|
"""Generates a responsive SVG bar chart for time-series data."""
|
||||||
if not data:
|
if not data:
|
||||||
return f"<h3>{html.escape(title)}</h3><p>No data yet.</p>"
|
return f"<h3>{html.escape(title)}</h3><p>No data yet.</p>"
|
||||||
@ -36,14 +43,14 @@ def generate_time_series_svg(title: str, data: list[tuple[str, int]], y_label: s
|
|||||||
svg_height, svg_width = 250, 600
|
svg_height, svg_width = 250, 600
|
||||||
bar_padding = 5
|
bar_padding = 5
|
||||||
bar_width = (svg_width - 50) / len(data) - bar_padding
|
bar_width = (svg_width - 50) / len(data) - bar_padding
|
||||||
|
|
||||||
bars = ""
|
bars = ""
|
||||||
labels = ""
|
labels = ""
|
||||||
for i, (key, val) in enumerate(data):
|
for i, (key, val) in enumerate(data):
|
||||||
bar_height = (val / max_val) * (svg_height - 50) if max_val > 0 else 0
|
bar_height = (val / max_val) * (svg_height - 50) if max_val > 0 else 0
|
||||||
x = i * (bar_width + bar_padding) + 40
|
x = i * (bar_width + bar_padding) + 40
|
||||||
y = svg_height - bar_height - 30
|
y = svg_height - bar_height - 30
|
||||||
|
|
||||||
bars += f'<rect x="{x}" y="{y}" width="{bar_width}" height="{bar_height}" fill="#007BFF"><title>{html.escape(key)}: {val}</title></rect>'
|
bars += f'<rect x="{x}" y="{y}" width="{bar_width}" height="{bar_height}" fill="#007BFF"><title>{html.escape(key)}: {val}</title></rect>'
|
||||||
labels += f'<text x="{x + bar_width / 2}" y="{svg_height - 15}" font-size="11" text-anchor="middle">{html.escape(key)}</text>'
|
labels += f'<text x="{x + bar_width / 2}" y="{svg_height - 15}" font-size="11" text-anchor="middle">{html.escape(key)}</text>'
|
||||||
|
|
||||||
@ -61,25 +68,32 @@ def generate_time_series_svg(title: str, data: list[tuple[str, int]], y_label: s
|
|||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def middleware(request, handler):
|
async def middleware(request, handler):
|
||||||
"""Middleware to count all incoming HTTP requests."""
|
"""Middleware to count all incoming HTTP requests."""
|
||||||
# Avoid counting requests to the stats page itself
|
# Avoid counting requests to the stats page itself
|
||||||
if request.path.startswith('/stats.html'):
|
if request.path.startswith("/stats.html"):
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
|
||||||
update_stats_counters(request.app['stats']['http_requests'], datetime.now(timezone.utc))
|
update_stats_counters(
|
||||||
|
request.app["stats"]["http_requests"], datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
|
||||||
|
|
||||||
def update_websocket_stats(app):
|
def update_websocket_stats(app):
|
||||||
update_stats_counters(app['stats']['websocket_requests'], datetime.now(timezone.utc))
|
update_stats_counters(
|
||||||
|
app["stats"]["websocket_requests"], datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def pipe_and_count_websocket(ws_from, ws_to, stats_dict):
|
async def pipe_and_count_websocket(ws_from, ws_to, stats_dict):
|
||||||
"""This function proxies WebSocket messages AND counts them."""
|
"""This function proxies WebSocket messages AND counts them."""
|
||||||
async for msg in ws_from:
|
async for msg in ws_from:
|
||||||
# This is the key part for monitoring WebSockets
|
# This is the key part for monitoring WebSockets
|
||||||
update_stats_counters(stats_dict, datetime.now(timezone.utc))
|
update_stats_counters(stats_dict, datetime.now(timezone.utc))
|
||||||
|
|
||||||
if msg.type == WSMsgType.TEXT:
|
if msg.type == WSMsgType.TEXT:
|
||||||
await ws_to.send_str(msg.data)
|
await ws_to.send_str(msg.data)
|
||||||
elif msg.type == WSMsgType.BINARY:
|
elif msg.type == WSMsgType.BINARY:
|
||||||
@ -91,27 +105,27 @@ async def pipe_and_count_websocket(ws_from, ws_to, stats_dict):
|
|||||||
|
|
||||||
async def stats_handler(request: web.Request):
|
async def stats_handler(request: web.Request):
|
||||||
"""Handler to display the statistics dashboard."""
|
"""Handler to display the statistics dashboard."""
|
||||||
stats = request.app['stats']
|
stats = request.app["stats"]
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
# Helper to prepare data for charts
|
# Helper to prepare data for charts
|
||||||
def get_data(source, period, count):
|
def get_data(source, period, count):
|
||||||
data = []
|
data = []
|
||||||
for i in range(count - 1, -1, -1):
|
for i in range(count - 1, -1, -1):
|
||||||
if period == 'hour':
|
if period == "hour":
|
||||||
dt = now - timedelta(hours=i)
|
dt = now - timedelta(hours=i)
|
||||||
key, label = dt.strftime('%Y-%m-%d-%H'), dt.strftime('%H:00')
|
key, label = dt.strftime("%Y-%m-%d-%H"), dt.strftime("%H:00")
|
||||||
data.append((label, source['by_hour'].get(key, 0)))
|
data.append((label, source["by_hour"].get(key, 0)))
|
||||||
elif period == 'day':
|
elif period == "day":
|
||||||
dt = now - timedelta(days=i)
|
dt = now - timedelta(days=i)
|
||||||
key, label = dt.strftime('%Y-%m-%d'), dt.strftime('%a')
|
key, label = dt.strftime("%Y-%m-%d"), dt.strftime("%a")
|
||||||
data.append((label, source['by_day'].get(key, 0)))
|
data.append((label, source["by_day"].get(key, 0)))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
http_hourly = get_data(stats['http_requests'], 'hour', 24)
|
http_hourly = get_data(stats["http_requests"], "hour", 24)
|
||||||
ws_hourly = get_data(stats['ws_messages'], 'hour', 24)
|
ws_hourly = get_data(stats["ws_messages"], "hour", 24)
|
||||||
http_daily = get_data(stats['http_requests'], 'day', 7)
|
http_daily = get_data(stats["http_requests"], "day", 7)
|
||||||
ws_daily = get_data(stats['ws_messages'], 'day', 7)
|
ws_daily = get_data(stats["ws_messages"], "day", 7)
|
||||||
|
|
||||||
body = f"""
|
body = f"""
|
||||||
<html><head><title>App Stats</title><meta http-equiv="refresh" content="30"></head>
|
<html><head><title>App Stats</title><meta http-equiv="refresh" content="30"></head>
|
||||||
@ -125,5 +139,4 @@ async def stats_handler(request: web.Request):
|
|||||||
{generate_time_series_svg("WebSocket Messages", ws_daily, "Msgs/Day")}
|
{generate_time_series_svg("WebSocket Messages", ws_daily, "Msgs/Day")}
|
||||||
</body></html>
|
</body></html>
|
||||||
"""
|
"""
|
||||||
return web.Response(text=body, content_type='text/html')
|
return web.Response(text=body, content_type="text/html")
|
||||||
|
|
||||||
|
|||||||
@ -81,27 +81,28 @@ emoji.EMOJI_DATA[
|
|||||||
|
|
||||||
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + ["picture"]
|
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + ["picture"]
|
||||||
|
|
||||||
|
|
||||||
def sanitize_html(value):
|
def sanitize_html(value):
|
||||||
|
|
||||||
soup = BeautifulSoup(value, 'html.parser')
|
soup = BeautifulSoup(value, "html.parser")
|
||||||
|
|
||||||
for script in soup.find_all('script'):
|
for script in soup.find_all("script"):
|
||||||
script.decompose()
|
script.decompose()
|
||||||
|
|
||||||
#for iframe in soup.find_all('iframe'):
|
# for iframe in soup.find_all('iframe'):
|
||||||
#iframe.decompose()
|
# iframe.decompose()
|
||||||
|
|
||||||
for tag in soup.find_all(['object', 'embed']):
|
for tag in soup.find_all(["object", "embed"]):
|
||||||
tag.decompose()
|
tag.decompose()
|
||||||
|
|
||||||
for tag in soup.find_all():
|
for tag in soup.find_all():
|
||||||
event_attributes = ['onclick', 'onerror', 'onload', 'onmouseover', 'onfocus']
|
event_attributes = ["onclick", "onerror", "onload", "onmouseover", "onfocus"]
|
||||||
for attr in event_attributes:
|
for attr in event_attributes:
|
||||||
if attr in tag.attrs:
|
if attr in tag.attrs:
|
||||||
del tag[attr]
|
del tag[attr]
|
||||||
|
|
||||||
for img in soup.find_all('img'):
|
for img in soup.find_all("img"):
|
||||||
if 'onerror' in img.attrs:
|
if "onerror" in img.attrs:
|
||||||
img.decompose()
|
img.decompose()
|
||||||
|
|
||||||
return soup.prettify()
|
return soup.prettify()
|
||||||
@ -126,6 +127,7 @@ def set_link_target_blank(text):
|
|||||||
|
|
||||||
return str(soup)
|
return str(soup)
|
||||||
|
|
||||||
|
|
||||||
def whitelist_attributes(html):
|
def whitelist_attributes(html):
|
||||||
return sanitize_html(html)
|
return sanitize_html(html)
|
||||||
|
|
||||||
|
|||||||
@ -13,23 +13,27 @@ from snek.system.view import BaseView
|
|||||||
|
|
||||||
register_heif_opener()
|
register_heif_opener()
|
||||||
|
|
||||||
from snek.view.drive import DriveApiView
|
from snek.view.drive import DriveApiView
|
||||||
|
|
||||||
|
|
||||||
class ChannelDriveApiView(DriveApiView):
|
class ChannelDriveApiView(DriveApiView):
|
||||||
|
|
||||||
login_required = True
|
login_required = True
|
||||||
|
|
||||||
async def get_target(self):
|
async def get_target(self):
|
||||||
target = await self.services.channel.get_home_folder(self.request.match_info.get("channel_uid"))
|
target = await self.services.channel.get_home_folder(
|
||||||
|
self.request.match_info.get("channel_uid")
|
||||||
|
)
|
||||||
target.mkdir(parents=True, exist_ok=True)
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
return target
|
return target
|
||||||
|
|
||||||
async def get_download_url(self, rel):
|
async def get_download_url(self, rel):
|
||||||
return f"/channel/{self.request.match_info.get('channel_uid')}/drive/{urllib.parse.quote(rel)}"
|
return f"/channel/{self.request.match_info.get('channel_uid')}/drive/{urllib.parse.quote(rel)}"
|
||||||
|
|
||||||
|
|
||||||
class ChannelAttachmentView(BaseView):
|
class ChannelAttachmentView(BaseView):
|
||||||
|
|
||||||
login_required=False
|
login_required = False
|
||||||
|
|
||||||
async def get(self):
|
async def get(self):
|
||||||
relative_path = self.request.match_info.get("relative_url")
|
relative_path = self.request.match_info.get("relative_url")
|
||||||
@ -114,7 +118,9 @@ class ChannelAttachmentView(BaseView):
|
|||||||
response.headers["Content-Disposition"] = (
|
response.headers["Content-Disposition"] = (
|
||||||
f'attachment; filename="{channel_attachment["name"]}"'
|
f'attachment; filename="{channel_attachment["name"]}"'
|
||||||
)
|
)
|
||||||
response.headers["Content-Type"] = original_format or "application/octet-stream"
|
response.headers["Content-Type"] = (
|
||||||
|
original_format or "application/octet-stream"
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def post(self):
|
async def post(self):
|
||||||
@ -164,11 +170,11 @@ class ChannelAttachmentView(BaseView):
|
|||||||
|
|
||||||
|
|
||||||
class ChannelAttachmentUploadView(BaseView):
|
class ChannelAttachmentUploadView(BaseView):
|
||||||
|
|
||||||
login_required = True
|
login_required = True
|
||||||
|
|
||||||
async def get(self):
|
async def get(self):
|
||||||
|
|
||||||
channel_uid = self.request.match_info.get("channel_uid")
|
channel_uid = self.request.match_info.get("channel_uid")
|
||||||
user_uid = self.request.session.get("uid")
|
user_uid = self.request.session.get("uid")
|
||||||
|
|
||||||
@ -184,37 +190,44 @@ class ChannelAttachmentUploadView(BaseView):
|
|||||||
|
|
||||||
file = None
|
file = None
|
||||||
filename = None
|
filename = None
|
||||||
|
|
||||||
|
|
||||||
msg = await ws.receive()
|
msg = await ws.receive()
|
||||||
if not msg.type == web.WSMsgType.TEXT:
|
if not msg.type == web.WSMsgType.TEXT:
|
||||||
return web.HTTPBadRequest()
|
return web.HTTPBadRequest()
|
||||||
|
|
||||||
data = msg.json()
|
data = msg.json()
|
||||||
if not data.get('type') == 'start':
|
if not data.get("type") == "start":
|
||||||
return web.HTTPBadRequest()
|
return web.HTTPBadRequest()
|
||||||
|
|
||||||
filename = data['filename']
|
filename = data["filename"]
|
||||||
attachment = await self.services.channel_attachment.create_file(
|
attachment = await self.services.channel_attachment.create_file(
|
||||||
channel_uid=channel_uid, name=filename, user_uid=user_uid
|
channel_uid=channel_uid, name=filename, user_uid=user_uid
|
||||||
)
|
)
|
||||||
pathlib.Path(attachment["path"]).parent.mkdir(parents=True, exist_ok=True)
|
pathlib.Path(attachment["path"]).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
async with aiofiles.open(attachment["path"], "wb") as file:
|
async with aiofiles.open(attachment["path"], "wb") as file:
|
||||||
print("File openend.", filename)
|
print("File openend.", filename)
|
||||||
async for msg in ws:
|
async for msg in ws:
|
||||||
if msg.type == web.WSMsgType.BINARY:
|
if msg.type == web.WSMsgType.BINARY:
|
||||||
print("Binary",filename)
|
print("Binary", filename)
|
||||||
if file is not None:
|
if file is not None:
|
||||||
await file.write(msg.data)
|
await file.write(msg.data)
|
||||||
await ws.send_json({"type": "progress", "filename": filename, "bytes": await file.tell()})
|
await ws.send_json(
|
||||||
|
{
|
||||||
|
"type": "progress",
|
||||||
|
"filename": filename,
|
||||||
|
"bytes": await file.tell(),
|
||||||
|
}
|
||||||
|
)
|
||||||
elif msg.type == web.WSMsgType.TEXT:
|
elif msg.type == web.WSMsgType.TEXT:
|
||||||
print("TExt",filename)
|
print("TExt", filename)
|
||||||
print(msg.json())
|
print(msg.json())
|
||||||
data = msg.json()
|
data = msg.json()
|
||||||
if data.get('type') == 'end':
|
if data.get("type") == "end":
|
||||||
relative_url = urllib.parse.quote(attachment["relative_url"])
|
relative_url = urllib.parse.quote(attachment["relative_url"])
|
||||||
await ws.send_json({"type": "done", "file": relative_url, "filename": filename})
|
await ws.send_json(
|
||||||
|
{"type": "done", "file": relative_url, "filename": filename}
|
||||||
|
)
|
||||||
elif msg.type == web.WSMsgType.ERROR:
|
elif msg.type == web.WSMsgType.ERROR:
|
||||||
break
|
break
|
||||||
return ws
|
return ws
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
from snek.system.view import BaseView
|
import functools
|
||||||
import functools
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
|
from snek.system.view import BaseView
|
||||||
|
|
||||||
|
|
||||||
class ContainerView(BaseView):
|
class ContainerView(BaseView):
|
||||||
|
|
||||||
login_required= True
|
login_required = True
|
||||||
|
|
||||||
async def stdout_event_handler(self, ws, data):
|
async def stdout_event_handler(self, ws, data):
|
||||||
try:
|
try:
|
||||||
@ -12,8 +15,8 @@ class ContainerView(BaseView):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(ex)
|
print(ex)
|
||||||
await ws.close()
|
await ws.close()
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def create_stdout_event_handler(self, ws):
|
async def create_stdout_event_handler(self, ws):
|
||||||
return functools.partial(self.stdout_event_handler, ws)
|
return functools.partial(self.stdout_event_handler, ws)
|
||||||
@ -26,23 +29,27 @@ class ContainerView(BaseView):
|
|||||||
return self.HTTPUnauthorized()
|
return self.HTTPUnauthorized()
|
||||||
|
|
||||||
channel_uid = self.request.match_info.get("channel_uid")
|
channel_uid = self.request.match_info.get("channel_uid")
|
||||||
channel_member = await self.services.channel_member.get(channel_uid=channel_uid, user_uid=self.request.session.get("uid"))
|
channel_member = await self.services.channel_member.get(
|
||||||
|
channel_uid=channel_uid, user_uid=self.request.session.get("uid")
|
||||||
|
)
|
||||||
if not channel_member:
|
if not channel_member:
|
||||||
return web.HTTPUnauthorized()
|
return web.HTTPUnauthorized()
|
||||||
|
|
||||||
container = await self.services.container.get(channel_uid)
|
container = await self.services.container.get(channel_uid)
|
||||||
if not container:
|
if not container:
|
||||||
return web.HTTPNotFound()
|
return web.HTTPNotFound()
|
||||||
|
|
||||||
if not container['status'] == 'running':
|
if not container["status"] == "running":
|
||||||
resp = await self.services.container.start(channel_uid)
|
await self.services.container.start(channel_uid)
|
||||||
await ws.send_bytes(b'Container is starting\n\n')
|
await ws.send_bytes(b"Container is starting\n\n")
|
||||||
|
|
||||||
container_name = await self.services.container.get_container_name(channel_uid)
|
container_name = await self.services.container.get_container_name(channel_uid)
|
||||||
|
|
||||||
event_handler = await self.create_stdout_event_handler(ws)
|
event_handler = await self.create_stdout_event_handler(ws)
|
||||||
|
|
||||||
await self.services.container.add_event_listener(container_name, "stdout", event_handler)
|
await self.services.container.add_event_listener(
|
||||||
|
container_name, "stdout", event_handler
|
||||||
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
data = await ws.receive()
|
data = await ws.receive()
|
||||||
@ -54,8 +61,8 @@ class ContainerView(BaseView):
|
|||||||
elif data.type == web.WSMsgType.ERROR:
|
elif data.type == web.WSMsgType.ERROR:
|
||||||
break
|
break
|
||||||
|
|
||||||
await self.services.container.remove_event_listener(container_name, channel_uid, "stdout")
|
await self.services.container.remove_event_listener(
|
||||||
|
container_name, channel_uid, "stdout"
|
||||||
return ws
|
)
|
||||||
|
|
||||||
|
|
||||||
|
return ws
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import quote, unquote
|
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
@ -42,7 +39,7 @@ class DriveApiView(BaseView):
|
|||||||
|
|
||||||
async def get(self):
|
async def get(self):
|
||||||
target = await self.get_target()
|
target = await self.get_target()
|
||||||
original_target = target
|
original_target = target
|
||||||
rel = self.request.query.get("path", "")
|
rel = self.request.query.get("path", "")
|
||||||
offset = int(self.request.query.get("offset", 0))
|
offset = int(self.request.query.get("offset", 0))
|
||||||
limit = int(self.request.query.get("limit", 20))
|
limit = int(self.request.query.get("limit", 20))
|
||||||
|
|||||||
@ -1,19 +1,15 @@
|
|||||||
# views/forum.py
|
# views/forum.py
|
||||||
from snek.system.view import BaseView
|
|
||||||
from aiohttp import web
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from snek.system.view import BaseView
|
from snek.system.view import BaseView
|
||||||
|
|
||||||
|
|
||||||
class ForumIndexView(BaseView):
|
class ForumIndexView(BaseView):
|
||||||
|
|
||||||
login_required = True
|
login_required = True
|
||||||
|
|
||||||
async def get(self):
|
async def get(self):
|
||||||
if self.login_required and not self.session.get("logged_in"):
|
if self.login_required and not self.session.get("logged_in"):
|
||||||
return web.HTTPFound("/")
|
return web.HTTPFound("/")
|
||||||
@ -71,407 +67,421 @@ class ForumIndexView(BaseView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ForumView(BaseView):
|
class ForumView(BaseView):
|
||||||
"""REST API endpoints for forum"""
|
"""REST API endpoints for forum"""
|
||||||
|
|
||||||
login_required = True
|
login_required = True
|
||||||
|
|
||||||
async def get_forums(self):
|
async def get_forums(self):
|
||||||
request = self
|
request = self
|
||||||
self = request.app
|
self = request.app
|
||||||
"""GET /forum/api/forums - Get all active forums"""
|
"""GET /forum/api/forums - Get all active forums"""
|
||||||
forums = []
|
forums = []
|
||||||
async for forum in self.services.forum.get_active_forums():
|
async for forum in self.services.forum.get_active_forums():
|
||||||
forums.append({
|
forums.append(
|
||||||
"uid": forum["uid"],
|
{
|
||||||
"name": forum["name"],
|
"uid": forum["uid"],
|
||||||
"description": forum["description"],
|
"name": forum["name"],
|
||||||
"slug": forum["slug"],
|
"description": forum["description"],
|
||||||
"icon": forum["icon"],
|
"slug": forum["slug"],
|
||||||
"thread_count": forum["thread_count"],
|
"icon": forum["icon"],
|
||||||
"post_count": forum["post_count"],
|
"thread_count": forum["thread_count"],
|
||||||
"last_post_at": forum["last_post_at"],
|
"post_count": forum["post_count"],
|
||||||
"last_thread_uid": forum["last_thread_uid"]
|
"last_post_at": forum["last_post_at"],
|
||||||
})
|
"last_thread_uid": forum["last_thread_uid"],
|
||||||
|
}
|
||||||
|
)
|
||||||
return web.json_response({"forums": forums})
|
return web.json_response({"forums": forums})
|
||||||
|
|
||||||
async def get_forum(self):
|
async def get_forum(self):
|
||||||
request = self
|
request = self
|
||||||
self = request.app
|
self = request.app
|
||||||
setattr(self, "request", request)
|
setattr(self, "request", request)
|
||||||
"""GET /forum/api/forums/:slug - Get forum by slug"""
|
"""GET /forum/api/forums/:slug - Get forum by slug"""
|
||||||
slug = self.request.match_info["slug"]
|
slug = self.request.match_info["slug"]
|
||||||
forum = await self.services.forum.get(slug=slug, is_active=True)
|
forum = await self.services.forum.get(slug=slug, is_active=True)
|
||||||
|
|
||||||
if not forum:
|
if not forum:
|
||||||
return web.json_response({"error": "Forum not found"}, status=404)
|
return web.json_response({"error": "Forum not found"}, status=404)
|
||||||
|
|
||||||
# Get threads
|
# Get threads
|
||||||
threads = []
|
threads = []
|
||||||
page = int(self.request.query.get("page", 1))
|
page = int(self.request.query.get("page", 1))
|
||||||
limit = 50
|
limit = 50
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
async for thread in forum.get_threads(limit=limit, offset=offset):
|
async for thread in forum.get_threads(limit=limit, offset=offset):
|
||||||
# Get author info
|
# Get author info
|
||||||
author = await self.services.user.get(uid=thread["created_by_uid"])
|
author = await self.services.user.get(uid=thread["created_by_uid"])
|
||||||
last_post_author = None
|
last_post_author = None
|
||||||
if thread["last_post_by_uid"]:
|
if thread["last_post_by_uid"]:
|
||||||
last_post_author = await self.services.user.get(uid=thread["last_post_by_uid"])
|
last_post_author = await self.services.user.get(
|
||||||
|
uid=thread["last_post_by_uid"]
|
||||||
threads.append({
|
)
|
||||||
"uid": thread["uid"],
|
|
||||||
"title": thread["title"],
|
threads.append(
|
||||||
"slug": thread["slug"],
|
{
|
||||||
"is_pinned": thread["is_pinned"],
|
"uid": thread["uid"],
|
||||||
"is_locked": thread["is_locked"],
|
"title": thread["title"],
|
||||||
"view_count": thread["view_count"],
|
"slug": thread["slug"],
|
||||||
"post_count": thread["post_count"],
|
"is_pinned": thread["is_pinned"],
|
||||||
"created_at": thread["created_at"],
|
"is_locked": thread["is_locked"],
|
||||||
"last_post_at": thread["last_post_at"],
|
"view_count": thread["view_count"],
|
||||||
"author": {
|
"post_count": thread["post_count"],
|
||||||
"uid": author["uid"],
|
"created_at": thread["created_at"],
|
||||||
"username": author["username"],
|
"last_post_at": thread["last_post_at"],
|
||||||
"nick": author["nick"],
|
"author": {
|
||||||
"color": author["color"]
|
"uid": author["uid"],
|
||||||
|
"username": author["username"],
|
||||||
|
"nick": author["nick"],
|
||||||
|
"color": author["color"],
|
||||||
|
},
|
||||||
|
"last_post_author": (
|
||||||
|
{
|
||||||
|
"uid": last_post_author["uid"],
|
||||||
|
"username": last_post_author["username"],
|
||||||
|
"nick": last_post_author["nick"],
|
||||||
|
"color": last_post_author["color"],
|
||||||
|
}
|
||||||
|
if last_post_author
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"forum": {
|
||||||
|
"uid": forum["uid"],
|
||||||
|
"name": forum["name"],
|
||||||
|
"description": forum["description"],
|
||||||
|
"slug": forum["slug"],
|
||||||
|
"icon": forum["icon"],
|
||||||
|
"thread_count": forum["thread_count"],
|
||||||
|
"post_count": forum["post_count"],
|
||||||
},
|
},
|
||||||
"last_post_author": {
|
"threads": threads,
|
||||||
"uid": last_post_author["uid"],
|
"page": page,
|
||||||
"username": last_post_author["username"],
|
"hasMore": len(threads) == limit,
|
||||||
"nick": last_post_author["nick"],
|
}
|
||||||
"color": last_post_author["color"]
|
)
|
||||||
} if last_post_author else None
|
|
||||||
})
|
|
||||||
|
|
||||||
return web.json_response({
|
|
||||||
"forum": {
|
|
||||||
"uid": forum["uid"],
|
|
||||||
"name": forum["name"],
|
|
||||||
"description": forum["description"],
|
|
||||||
"slug": forum["slug"],
|
|
||||||
"icon": forum["icon"],
|
|
||||||
"thread_count": forum["thread_count"],
|
|
||||||
"post_count": forum["post_count"]
|
|
||||||
},
|
|
||||||
"threads": threads,
|
|
||||||
"page": page,
|
|
||||||
"hasMore": len(threads) == limit
|
|
||||||
})
|
|
||||||
|
|
||||||
async def create_thread(self):
|
async def create_thread(self):
|
||||||
request = self
|
request = self
|
||||||
self = request.app
|
self = request.app
|
||||||
|
|
||||||
setattr(self, "request", request)
|
setattr(self, "request", request)
|
||||||
"""POST /forum/api/forums/:slug/threads - Create new thread"""
|
"""POST /forum/api/forums/:slug/threads - Create new thread"""
|
||||||
if not self.request.session.get("logged_in"):
|
if not self.request.session.get("logged_in"):
|
||||||
return web.json_response({"error": "Unauthorized"}, status=401)
|
return web.json_response({"error": "Unauthorized"}, status=401)
|
||||||
|
|
||||||
slug = self.request.match_info["slug"]
|
slug = self.request.match_info["slug"]
|
||||||
forum = await self.services.forum.get(slug=slug, is_active=True)
|
forum = await self.services.forum.get(slug=slug, is_active=True)
|
||||||
|
|
||||||
if not forum:
|
if not forum:
|
||||||
return web.json_response({"error": "Forum not found"}, status=404)
|
return web.json_response({"error": "Forum not found"}, status=404)
|
||||||
|
|
||||||
data = await self.request.json()
|
data = await self.request.json()
|
||||||
title = data.get("title", "").strip()
|
title = data.get("title", "").strip()
|
||||||
content = data.get("content", "").strip()
|
content = data.get("content", "").strip()
|
||||||
|
|
||||||
if not title or not content:
|
if not title or not content:
|
||||||
return web.json_response({"error": "Title and content required"}, status=400)
|
return web.json_response(
|
||||||
|
{"error": "Title and content required"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
thread, post = await self.services.thread.create_thread(
|
thread, post = await self.services.thread.create_thread(
|
||||||
forum_uid=forum["uid"],
|
forum_uid=forum["uid"],
|
||||||
title=title,
|
title=title,
|
||||||
content=content,
|
content=content,
|
||||||
created_by_uid=self.request.session["uid"]
|
created_by_uid=self.request.session["uid"],
|
||||||
)
|
)
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response(
|
||||||
"thread": {
|
{
|
||||||
"uid": thread["uid"],
|
"thread": {
|
||||||
"slug": thread["slug"],
|
"uid": thread["uid"],
|
||||||
"forum_slug": forum["slug"]
|
"slug": thread["slug"],
|
||||||
|
"forum_slug": forum["slug"],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return web.json_response({"error": str(e)}, status=400)
|
return web.json_response({"error": str(e)}, status=400)
|
||||||
|
|
||||||
async def get_thread(self):
|
async def get_thread(self):
|
||||||
request = self
|
request = self
|
||||||
self = request.app
|
self = request.app
|
||||||
|
|
||||||
setattr(self, "request", request)
|
setattr(self, "request", request)
|
||||||
"""GET /forum/api/threads/:thread_slug - Get thread with posts"""
|
"""GET /forum/api/threads/:thread_slug - Get thread with posts"""
|
||||||
thread_slug = self.request.match_info["thread_slug"]
|
thread_slug = self.request.match_info["thread_slug"]
|
||||||
thread = await self.services.thread.get(slug=thread_slug)
|
thread = await self.services.thread.get(slug=thread_slug)
|
||||||
|
|
||||||
if not thread:
|
if not thread:
|
||||||
return web.json_response({"error": "Thread not found"}, status=404)
|
return web.json_response({"error": "Thread not found"}, status=404)
|
||||||
|
|
||||||
# Increment view count
|
# Increment view count
|
||||||
await thread.increment_view_count()
|
await thread.increment_view_count()
|
||||||
|
|
||||||
# Get forum
|
# Get forum
|
||||||
forum = await self.services.forum.get(uid=thread["forum_uid"])
|
forum = await self.services.forum.get(uid=thread["forum_uid"])
|
||||||
|
|
||||||
# Get posts
|
# Get posts
|
||||||
posts = []
|
posts = []
|
||||||
page = int(self.request.query.get("page", 1))
|
page = int(self.request.query.get("page", 1))
|
||||||
limit = 50
|
limit = 50
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
current_user_uid = self.request.session.get("uid")
|
current_user_uid = self.request.session.get("uid")
|
||||||
|
|
||||||
async for post in thread.get_posts(limit=limit, offset=offset):
|
async for post in thread.get_posts(limit=limit, offset=offset):
|
||||||
author = await post.get_author()
|
author = await post.get_author()
|
||||||
is_liked = False
|
is_liked = False
|
||||||
if current_user_uid:
|
if current_user_uid:
|
||||||
is_liked = await post.is_liked_by(current_user_uid)
|
is_liked = await post.is_liked_by(current_user_uid)
|
||||||
|
|
||||||
posts.append({
|
posts.append(
|
||||||
"uid": post["uid"],
|
{
|
||||||
"content": post["content"],
|
"uid": post["uid"],
|
||||||
"created_at": post["created_at"],
|
"content": post["content"],
|
||||||
"edited_at": post["edited_at"],
|
"created_at": post["created_at"],
|
||||||
"is_first_post": post["is_first_post"],
|
"edited_at": post["edited_at"],
|
||||||
"like_count": post["like_count"],
|
"is_first_post": post["is_first_post"],
|
||||||
"is_liked": is_liked,
|
"like_count": post["like_count"],
|
||||||
"author": {
|
"is_liked": is_liked,
|
||||||
"uid": author["uid"],
|
"author": {
|
||||||
"username": author["username"],
|
"uid": author["uid"],
|
||||||
"nick": author["nick"],
|
"username": author["username"],
|
||||||
"color": author["color"]
|
"nick": author["nick"],
|
||||||
|
"color": author["color"],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
# Get thread author
|
# Get thread author
|
||||||
thread_author = await self.services.user.get(uid=thread["created_by_uid"])
|
thread_author = await self.services.user.get(uid=thread["created_by_uid"])
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response(
|
||||||
"thread": {
|
{
|
||||||
"uid": thread["uid"],
|
"thread": {
|
||||||
"title": thread["title"],
|
"uid": thread["uid"],
|
||||||
"slug": thread["slug"],
|
"title": thread["title"],
|
||||||
"is_pinned": thread["is_pinned"],
|
"slug": thread["slug"],
|
||||||
"is_locked": thread["is_locked"],
|
"is_pinned": thread["is_pinned"],
|
||||||
"view_count": thread["view_count"],
|
"is_locked": thread["is_locked"],
|
||||||
"post_count": thread["post_count"],
|
"view_count": thread["view_count"],
|
||||||
"created_at": thread["created_at"],
|
"post_count": thread["post_count"],
|
||||||
"author": {
|
"created_at": thread["created_at"],
|
||||||
"uid": thread_author["uid"],
|
"author": {
|
||||||
"username": thread_author["username"],
|
"uid": thread_author["uid"],
|
||||||
"nick": thread_author["nick"],
|
"username": thread_author["username"],
|
||||||
"color": thread_author["color"]
|
"nick": thread_author["nick"],
|
||||||
}
|
"color": thread_author["color"],
|
||||||
},
|
},
|
||||||
"forum": {
|
},
|
||||||
"uid": forum["uid"],
|
"forum": {
|
||||||
"name": forum["name"],
|
"uid": forum["uid"],
|
||||||
"slug": forum["slug"]
|
"name": forum["name"],
|
||||||
},
|
"slug": forum["slug"],
|
||||||
"posts": posts,
|
},
|
||||||
"page": page,
|
"posts": posts,
|
||||||
"hasMore": len(posts) == limit
|
"page": page,
|
||||||
})
|
"hasMore": len(posts) == limit,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async def create_post(self):
|
async def create_post(self):
|
||||||
request = self
|
request = self
|
||||||
self = request.app
|
self = request.app
|
||||||
|
|
||||||
setattr(self, "request", request)
|
setattr(self, "request", request)
|
||||||
"""POST /forum/api/threads/:thread_uid/posts - Create new post"""
|
"""POST /forum/api/threads/:thread_uid/posts - Create new post"""
|
||||||
if not self.request.session.get("logged_in"):
|
if not self.request.session.get("logged_in"):
|
||||||
return web.json_response({"error": "Unauthorized"}, status=401)
|
return web.json_response({"error": "Unauthorized"}, status=401)
|
||||||
|
|
||||||
thread_uid = self.request.match_info["thread_uid"]
|
thread_uid = self.request.match_info["thread_uid"]
|
||||||
thread = await self.services.thread.get(uid=thread_uid)
|
thread = await self.services.thread.get(uid=thread_uid)
|
||||||
|
|
||||||
if not thread:
|
if not thread:
|
||||||
return web.json_response({"error": "Thread not found"}, status=404)
|
return web.json_response({"error": "Thread not found"}, status=404)
|
||||||
|
|
||||||
data = await self.request.json()
|
data = await self.request.json()
|
||||||
content = data.get("content", "").strip()
|
content = data.get("content", "").strip()
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return web.json_response({"error": "Content required"}, status=400)
|
return web.json_response({"error": "Content required"}, status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
post = await self.services.post.create_post(
|
post = await self.services.post.create_post(
|
||||||
thread_uid=thread["uid"],
|
thread_uid=thread["uid"],
|
||||||
forum_uid=thread["forum_uid"],
|
forum_uid=thread["forum_uid"],
|
||||||
content=content,
|
content=content,
|
||||||
created_by_uid=self.request.session["uid"]
|
created_by_uid=self.request.session["uid"],
|
||||||
)
|
)
|
||||||
|
|
||||||
author = await post.get_author()
|
author = await post.get_author()
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response(
|
||||||
"post": {
|
{
|
||||||
"uid": post["uid"],
|
"post": {
|
||||||
"content": post["content"],
|
"uid": post["uid"],
|
||||||
"created_at": post["created_at"],
|
"content": post["content"],
|
||||||
"like_count": post["like_count"],
|
"created_at": post["created_at"],
|
||||||
"is_liked": False,
|
"like_count": post["like_count"],
|
||||||
"author": {
|
"is_liked": False,
|
||||||
"uid": author["uid"],
|
"author": {
|
||||||
"username": author["username"],
|
"uid": author["uid"],
|
||||||
"nick": author["nick"],
|
"username": author["username"],
|
||||||
"color": author["color"]
|
"nick": author["nick"],
|
||||||
|
"color": author["color"],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return web.json_response({"error": str(e)}, status=400)
|
return web.json_response({"error": str(e)}, status=400)
|
||||||
|
|
||||||
async def edit_post(self):
|
async def edit_post(self):
|
||||||
request = self
|
request = self
|
||||||
self = request.app
|
self = request.app
|
||||||
|
|
||||||
setattr(self, "request", request)
|
setattr(self, "request", request)
|
||||||
"""PUT /forum/api/posts/:post_uid - Edit post"""
|
"""PUT /forum/api/posts/:post_uid - Edit post"""
|
||||||
if not self.request.session.get("logged_in"):
|
if not self.request.session.get("logged_in"):
|
||||||
return web.json_response({"error": "Unauthorized"}, status=401)
|
return web.json_response({"error": "Unauthorized"}, status=401)
|
||||||
|
|
||||||
post_uid = self.request.match_info["post_uid"]
|
post_uid = self.request.match_info["post_uid"]
|
||||||
data = await self.request.json()
|
data = await self.request.json()
|
||||||
content = data.get("content", "").strip()
|
content = data.get("content", "").strip()
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return web.json_response({"error": "Content required"}, status=400)
|
return web.json_response({"error": "Content required"}, status=400)
|
||||||
|
|
||||||
post = await self.services.post.edit_post(
|
post = await self.services.post.edit_post(
|
||||||
post_uid=post_uid,
|
post_uid=post_uid, content=content, user_uid=self.request.session["uid"]
|
||||||
content=content,
|
|
||||||
user_uid=self.request.session["uid"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not post:
|
if not post:
|
||||||
return web.json_response({"error": "Cannot edit post"}, status=403)
|
return web.json_response({"error": "Cannot edit post"}, status=403)
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response(
|
||||||
"post": {
|
{
|
||||||
"uid": post["uid"],
|
"post": {
|
||||||
"content": post["content"],
|
"uid": post["uid"],
|
||||||
"edited_at": post["edited_at"]
|
"content": post["content"],
|
||||||
|
"edited_at": post["edited_at"],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
async def delete_post(self):
|
async def delete_post(self):
|
||||||
request = self
|
request = self
|
||||||
self = request.app
|
self = request.app
|
||||||
|
|
||||||
setattr(self, "request", request)
|
setattr(self, "request", request)
|
||||||
"""DELETE /forum/api/posts/:post_uid - Delete post"""
|
"""DELETE /forum/api/posts/:post_uid - Delete post"""
|
||||||
if not self.request.session.get("logged_in"):
|
if not self.request.session.get("logged_in"):
|
||||||
return web.json_response({"error": "Unauthorized"}, status=401)
|
return web.json_response({"error": "Unauthorized"}, status=401)
|
||||||
|
|
||||||
post_uid = self.request.match_info["post_uid"]
|
post_uid = self.request.match_info["post_uid"]
|
||||||
|
|
||||||
success = await self.services.post.delete_post(
|
success = await self.services.post.delete_post(
|
||||||
post_uid=post_uid,
|
post_uid=post_uid, user_uid=self.request.session["uid"]
|
||||||
user_uid=self.request.session["uid"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
return web.json_response({"error": "Cannot delete post"}, status=403)
|
return web.json_response({"error": "Cannot delete post"}, status=403)
|
||||||
|
|
||||||
return web.json_response({"success": True})
|
return web.json_response({"success": True})
|
||||||
|
|
||||||
async def toggle_like(self):
|
async def toggle_like(self):
|
||||||
request = self
|
request = self
|
||||||
self = request.app
|
self = request.app
|
||||||
|
|
||||||
setattr(self, "request", request)
|
setattr(self, "request", request)
|
||||||
"""POST /forum/api/posts/:post_uid/like - Toggle post like"""
|
"""POST /forum/api/posts/:post_uid/like - Toggle post like"""
|
||||||
if not self.request.session.get("logged_in"):
|
if not self.request.session.get("logged_in"):
|
||||||
return web.json_response({"error": "Unauthorized"}, status=401)
|
return web.json_response({"error": "Unauthorized"}, status=401)
|
||||||
|
|
||||||
post_uid = self.request.match_info["post_uid"]
|
post_uid = self.request.match_info["post_uid"]
|
||||||
|
|
||||||
is_liked = await self.services.post_like.toggle_like(
|
is_liked = await self.services.post_like.toggle_like(
|
||||||
post_uid=post_uid,
|
post_uid=post_uid, user_uid=self.request.session["uid"]
|
||||||
user_uid=self.request.session["uid"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_liked is None:
|
if is_liked is None:
|
||||||
return web.json_response({"error": "Failed to toggle like"}, status=400)
|
return web.json_response({"error": "Failed to toggle like"}, status=400)
|
||||||
|
|
||||||
# Get updated post
|
# Get updated post
|
||||||
post = await self.services.post.get(uid=post_uid)
|
post = await self.services.post.get(uid=post_uid)
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response(
|
||||||
"is_liked": is_liked,
|
{"is_liked": is_liked, "like_count": post["like_count"]}
|
||||||
"like_count": post["like_count"]
|
)
|
||||||
})
|
|
||||||
|
|
||||||
async def toggle_pin(self):
|
async def toggle_pin(self):
|
||||||
request = self
|
request = self
|
||||||
self = request.app
|
self = request.app
|
||||||
|
|
||||||
setattr(self, "request", request)
|
setattr(self, "request", request)
|
||||||
"""POST /forum/api/threads/:thread_uid/pin - Toggle thread pin"""
|
"""POST /forum/api/threads/:thread_uid/pin - Toggle thread pin"""
|
||||||
if not self.request.session.get("logged_in"):
|
if not self.request.session.get("logged_in"):
|
||||||
return web.json_response({"error": "Unauthorized"}, status=401)
|
return web.json_response({"error": "Unauthorized"}, status=401)
|
||||||
|
|
||||||
thread_uid = self.request.match_info["thread_uid"]
|
thread_uid = self.request.match_info["thread_uid"]
|
||||||
|
|
||||||
thread = await self.services.thread.toggle_pin(
|
thread = await self.services.thread.toggle_pin(
|
||||||
thread_uid=thread_uid,
|
thread_uid=thread_uid, user_uid=self.request.session["uid"]
|
||||||
user_uid=self.request.session["uid"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not thread:
|
if not thread:
|
||||||
return web.json_response({"error": "Cannot toggle pin"}, status=403)
|
return web.json_response({"error": "Cannot toggle pin"}, status=403)
|
||||||
|
|
||||||
return web.json_response({"is_pinned": thread["is_pinned"]})
|
return web.json_response({"is_pinned": thread["is_pinned"]})
|
||||||
|
|
||||||
async def toggle_lock(self):
|
async def toggle_lock(self):
|
||||||
request = self
|
request = self
|
||||||
self = request.app
|
self = request.app
|
||||||
|
|
||||||
setattr(self, "request", request)
|
setattr(self, "request", request)
|
||||||
"""POST /forum/api/threads/:thread_uid/lock - Toggle thread lock"""
|
"""POST /forum/api/threads/:thread_uid/lock - Toggle thread lock"""
|
||||||
if not self.request.session.get("logged_in"):
|
if not self.request.session.get("logged_in"):
|
||||||
return web.json_response({"error": "Unauthorized"}, status=401)
|
return web.json_response({"error": "Unauthorized"}, status=401)
|
||||||
|
|
||||||
thread_uid = self.request.match_info["thread_uid"]
|
thread_uid = self.request.match_info["thread_uid"]
|
||||||
|
|
||||||
thread = await self.services.thread.toggle_lock(
|
thread = await self.services.thread.toggle_lock(
|
||||||
thread_uid=thread_uid,
|
thread_uid=thread_uid, user_uid=self.request.session["uid"]
|
||||||
user_uid=self.request.session["uid"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not thread:
|
if not thread:
|
||||||
return web.json_response({"error": "Cannot toggle lock"}, status=403)
|
return web.json_response({"error": "Cannot toggle lock"}, status=403)
|
||||||
|
|
||||||
return web.json_response({"is_locked": thread["is_locked"]})
|
return web.json_response({"is_locked": thread["is_locked"]})
|
||||||
|
|
||||||
|
|
||||||
# views/forum_websocket.py
|
# views/forum_websocket.py
|
||||||
class ForumWebSocketView(BaseView):
|
class ForumWebSocketView(BaseView):
|
||||||
"""WebSocket view for real-time forum updates"""
|
"""WebSocket view for real-time forum updates"""
|
||||||
|
|
||||||
async def get(self):
|
async def get(self):
|
||||||
ws = web.WebSocketResponse()
|
ws = web.WebSocketResponse()
|
||||||
await ws.prepare(self.request)
|
await ws.prepare(self.request)
|
||||||
|
|
||||||
# Store WebSocket connection
|
# Store WebSocket connection
|
||||||
ws_id = self.services.forum.generate_uid()
|
ws_id = self.services.forum.generate_uid()
|
||||||
if not hasattr(self.app, 'forum_websockets'):
|
if not hasattr(self.app, "forum_websockets"):
|
||||||
self.app.forum_websockets = {}
|
self.app.forum_websockets = {}
|
||||||
|
|
||||||
user_uid = self.request.session.get("uid")
|
user_uid = self.request.session.get("uid")
|
||||||
self.app.forum_websockets[ws_id] = {
|
self.app.forum_websockets[ws_id] = {
|
||||||
"ws": ws,
|
"ws": ws,
|
||||||
"user_uid": user_uid,
|
"user_uid": user_uid,
|
||||||
"subscriptions": set()
|
"subscriptions": set(),
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async for msg in ws:
|
async for msg in ws:
|
||||||
if msg.type == web.WSMsgType.TEXT:
|
if msg.type == web.WSMsgType.TEXT:
|
||||||
@ -483,68 +493,76 @@ class ForumWebSocketView(BaseView):
|
|||||||
# Clean up
|
# Clean up
|
||||||
if ws_id in self.app.forum_websockets:
|
if ws_id in self.app.forum_websockets:
|
||||||
del self.app.forum_websockets[ws_id]
|
del self.app.forum_websockets[ws_id]
|
||||||
|
|
||||||
return ws
|
return ws
|
||||||
|
|
||||||
async def handle_ws_message(self, ws_id, data):
|
async def handle_ws_message(self, ws_id, data):
|
||||||
"""Handle incoming WebSocket messages"""
|
"""Handle incoming WebSocket messages"""
|
||||||
action = data.get("action")
|
action = data.get("action")
|
||||||
|
|
||||||
if action == "subscribe":
|
if action == "subscribe":
|
||||||
# Subscribe to forum/thread updates
|
# Subscribe to forum/thread updates
|
||||||
target_type = data.get("type") # "forum" or "thread"
|
target_type = data.get("type") # "forum" or "thread"
|
||||||
target_id = data.get("id")
|
target_id = data.get("id")
|
||||||
|
|
||||||
if target_type and target_id:
|
if target_type and target_id:
|
||||||
subscription = f"{target_type}:{target_id}"
|
subscription = f"{target_type}:{target_id}"
|
||||||
self.app.forum_websockets[ws_id]["subscriptions"].add(subscription)
|
self.app.forum_websockets[ws_id]["subscriptions"].add(subscription)
|
||||||
|
|
||||||
# Send confirmation
|
# Send confirmation
|
||||||
ws = self.app.forum_websockets[ws_id]["ws"]
|
ws = self.app.forum_websockets[ws_id]["ws"]
|
||||||
await ws.send_str(json.dumps({
|
await ws.send_str(
|
||||||
"type": "subscribed",
|
json.dumps({"type": "subscribed", "subscription": subscription})
|
||||||
"subscription": subscription
|
)
|
||||||
}))
|
|
||||||
|
|
||||||
elif action == "unsubscribe":
|
elif action == "unsubscribe":
|
||||||
target_type = data.get("type")
|
target_type = data.get("type")
|
||||||
target_id = data.get("id")
|
target_id = data.get("id")
|
||||||
|
|
||||||
if target_type and target_id:
|
if target_type and target_id:
|
||||||
subscription = f"{target_type}:{target_id}"
|
subscription = f"{target_type}:{target_id}"
|
||||||
self.app.forum_websockets[ws_id]["subscriptions"].discard(subscription)
|
self.app.forum_websockets[ws_id]["subscriptions"].discard(subscription)
|
||||||
|
|
||||||
# Send confirmation
|
# Send confirmation
|
||||||
ws = self.app.forum_websockets[ws_id]["ws"]
|
ws = self.app.forum_websockets[ws_id]["ws"]
|
||||||
await ws.send_str(json.dumps({
|
await ws.send_str(
|
||||||
"type": "unsubscribed",
|
json.dumps({"type": "unsubscribed", "subscription": subscription})
|
||||||
"subscription": subscription
|
)
|
||||||
}))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def broadcast_update(app, event_type, data):
|
async def broadcast_update(app, event_type, data):
|
||||||
"""Broadcast updates to subscribed WebSocket clients"""
|
"""Broadcast updates to subscribed WebSocket clients"""
|
||||||
if not hasattr(app, 'forum_websockets'):
|
if not hasattr(app, "forum_websockets"):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Determine subscription targets based on event
|
# Determine subscription targets based on event
|
||||||
targets = set()
|
targets = set()
|
||||||
|
|
||||||
if event_type in ["thread_created", "post_created", "post_edited", "post_deleted"]:
|
if event_type in [
|
||||||
|
"thread_created",
|
||||||
|
"post_created",
|
||||||
|
"post_edited",
|
||||||
|
"post_deleted",
|
||||||
|
]:
|
||||||
if "forum_uid" in data:
|
if "forum_uid" in data:
|
||||||
targets.add(f"forum:{data['forum_uid']}")
|
targets.add(f"forum:{data['forum_uid']}")
|
||||||
|
|
||||||
if event_type in ["post_created", "post_edited", "post_deleted", "post_liked", "post_unliked"]:
|
if event_type in [
|
||||||
|
"post_created",
|
||||||
|
"post_edited",
|
||||||
|
"post_deleted",
|
||||||
|
"post_liked",
|
||||||
|
"post_unliked",
|
||||||
|
]:
|
||||||
if "thread_uid" in data:
|
if "thread_uid" in data:
|
||||||
targets.add(f"thread:{data['thread_uid']}")
|
targets.add(f"thread:{data['thread_uid']}")
|
||||||
|
|
||||||
# Send to subscribed clients
|
# Send to subscribed clients
|
||||||
for ws_id, ws_data in app.forum_websockets.items():
|
for ws_id, ws_data in app.forum_websockets.items():
|
||||||
if ws_data["subscriptions"] & targets:
|
if ws_data["subscriptions"] & targets:
|
||||||
try:
|
try:
|
||||||
await ws_data["ws"].send_str(json.dumps({
|
await ws_data["ws"].send_str(
|
||||||
"type": event_type,
|
json.dumps({"type": event_type, "data": data})
|
||||||
"data": data
|
)
|
||||||
}))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -30,7 +30,7 @@ from snek.system.view import BaseFormView
|
|||||||
|
|
||||||
|
|
||||||
class RegisterFormView(BaseFormView):
|
class RegisterFormView(BaseFormView):
|
||||||
|
|
||||||
login_required = False
|
login_required = False
|
||||||
|
|
||||||
form = RegisterForm
|
form = RegisterForm
|
||||||
|
|||||||
@ -6,18 +6,18 @@
|
|||||||
|
|
||||||
# MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions.
|
# MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions.
|
||||||
|
|
||||||
from snek.system.stats import update_websocket_stats
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import random
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from snek.system.model import now
|
from snek.system.model import now
|
||||||
from snek.system.profiler import Profiler
|
from snek.system.stats import update_websocket_stats
|
||||||
from snek.system.view import BaseView
|
from snek.system.view import BaseView
|
||||||
import time
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -203,7 +203,7 @@ class RPCView(BaseView):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
return channels
|
return channels
|
||||||
|
|
||||||
async def clear_channel(self, channel_uid):
|
async def clear_channel(self, channel_uid):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
user = await self.services.user.get(uid=self.user_uid)
|
user = await self.services.user.get(uid=self.user_uid)
|
||||||
@ -211,41 +211,49 @@ class RPCView(BaseView):
|
|||||||
raise Exception("Not allowed")
|
raise Exception("Not allowed")
|
||||||
return await self.services.channel_message.clear(channel_uid)
|
return await self.services.channel_message.clear(channel_uid)
|
||||||
|
|
||||||
async def write_container(self, channel_uid, content,timeout=3):
|
async def write_container(self, channel_uid, content, timeout=3):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
channel_member = await self.services.channel_member.get(
|
channel_member = await self.services.channel_member.get(
|
||||||
channel_uid=channel_uid, user_uid=self.user_uid
|
channel_uid=channel_uid, user_uid=self.user_uid
|
||||||
)
|
)
|
||||||
if not channel_member:
|
if not channel_member:
|
||||||
raise Exception("Not allowed")
|
raise Exception("Not allowed")
|
||||||
|
|
||||||
container_name = await self.services.container.get_container_name(channel_uid)
|
container_name = await self.services.container.get_container_name(
|
||||||
|
channel_uid
|
||||||
|
)
|
||||||
|
|
||||||
class SessionCall:
|
class SessionCall:
|
||||||
|
|
||||||
def __init__(self, app,channel_uid_uid, container_name):
|
def __init__(self, app, channel_uid_uid, container_name):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.channel_uid = channel_uid
|
self.channel_uid = channel_uid
|
||||||
self.container_name = container_name
|
self.container_name = container_name
|
||||||
self.time_last_output = time.time()
|
self.time_last_output = time.time()
|
||||||
self.output = b''
|
self.output = b""
|
||||||
|
|
||||||
async def stdout_event_handler(self, data):
|
async def stdout_event_handler(self, data):
|
||||||
self.time_last_output = time.time()
|
self.time_last_output = time.time()
|
||||||
self.output += data
|
self.output += data
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def communicate(self,content, timeout=3):
|
async def communicate(self, content, timeout=3):
|
||||||
await self.app.services.container.add_event_listener(self.container_name, "stdout", self.stdout_event_handler)
|
await self.app.services.container.add_event_listener(
|
||||||
await self.app.services.container.write_stdin(self.channel_uid, content)
|
self.container_name, "stdout", self.stdout_event_handler
|
||||||
|
)
|
||||||
|
await self.app.services.container.write_stdin(
|
||||||
|
self.channel_uid, content
|
||||||
|
)
|
||||||
|
|
||||||
while time.time() - self.time_last_output < timeout:
|
while time.time() - self.time_last_output < timeout:
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
await self.app.services.container.remove_event_listener(self.container_name, "stdout", self.stdout_event_handler)
|
await self.app.services.container.remove_event_listener(
|
||||||
|
self.container_name, "stdout", self.stdout_event_handler
|
||||||
|
)
|
||||||
return self.output
|
return self.output
|
||||||
|
|
||||||
sc = SessionCall(self, channel_uid,container_name)
|
sc = SessionCall(self, channel_uid, container_name)
|
||||||
return (await sc.communicate(content)).decode("utf-8","ignore")
|
return (await sc.communicate(content)).decode("utf-8", "ignore")
|
||||||
|
|
||||||
async def get_container(self, channel_uid):
|
async def get_container(self, channel_uid):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
@ -258,44 +266,44 @@ class RPCView(BaseView):
|
|||||||
result = None
|
result = None
|
||||||
if container:
|
if container:
|
||||||
result = {
|
result = {
|
||||||
"name": await self.services.container.get_container_name(channel_uid),
|
"name": await self.services.container.get_container_name(
|
||||||
|
channel_uid
|
||||||
|
),
|
||||||
"cpus": container["deploy"]["resources"]["limits"]["cpus"],
|
"cpus": container["deploy"]["resources"]["limits"]["cpus"],
|
||||||
"memory": container["deploy"]["resources"]["limits"]["memory"],
|
"memory": container["deploy"]["resources"]["limits"]["memory"],
|
||||||
"image": "ubuntu:latest",
|
"image": "ubuntu:latest",
|
||||||
"volumes": [],
|
"volumes": [],
|
||||||
"status": container["status"]
|
"status": container["status"],
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def _finalize_message_task(self, message_uid):
|
||||||
async def _finalize_message_task(self,message_uid):
|
|
||||||
try:
|
try:
|
||||||
for _ in range(7):
|
for _ in range(7):
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
if not self._finalize_task:
|
if not self._finalize_task:
|
||||||
return
|
return
|
||||||
except asyncio.exceptions.CancelledError:
|
except asyncio.exceptions.CancelledError:
|
||||||
print("Finalization cancelled.")
|
print("Finalization cancelled.")
|
||||||
return
|
return
|
||||||
|
|
||||||
self._finalize_task = None
|
self._finalize_task = None
|
||||||
message = await self.services.channel_message.get(message_uid, is_final=False)
|
message = await self.services.channel_message.get(
|
||||||
|
message_uid, is_final=False
|
||||||
|
)
|
||||||
if not message:
|
if not message:
|
||||||
return False
|
return False
|
||||||
if message["user_uid"] != self.user_uid:
|
if message["user_uid"] != self.user_uid:
|
||||||
raise Exception("Not allowed")
|
raise Exception("Not allowed")
|
||||||
|
|
||||||
if message["is_final"]:
|
if message["is_final"]:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if not message["is_final"]:
|
if not message["is_final"]:
|
||||||
await self.services.chat.finalize(message["uid"])
|
await self.services.chat.finalize(message["uid"])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def _queue_finalize_message(self, message_uid):
|
async def _queue_finalize_message(self, message_uid):
|
||||||
if self._finalize_task:
|
if self._finalize_task:
|
||||||
self._finalize_task.cancel()
|
self._finalize_task.cancel()
|
||||||
@ -305,18 +313,22 @@ class RPCView(BaseView):
|
|||||||
|
|
||||||
async def send_message(self, channel_uid, message, is_final=True):
|
async def send_message(self, channel_uid, message, is_final=True):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
|
|
||||||
message = message.strip()
|
message = message.strip()
|
||||||
|
|
||||||
if not is_final:
|
if not is_final:
|
||||||
if_final = False
|
check_message = await self.services.channel_message.get(
|
||||||
check_message = await self.services.channel_message.get(channel_uid=channel_uid, user_uid=self.user_uid,is_final=False,deleted_at=None)
|
channel_uid=channel_uid,
|
||||||
|
user_uid=self.user_uid,
|
||||||
|
is_final=False,
|
||||||
|
deleted_at=None,
|
||||||
|
)
|
||||||
if check_message:
|
if check_message:
|
||||||
await self._queue_finalize_message(check_message["uid"])
|
await self._queue_finalize_message(check_message["uid"])
|
||||||
return await self.update_message_text(check_message["uid"], message)
|
return await self.update_message_text(check_message["uid"], message)
|
||||||
|
|
||||||
if not message:
|
if not message:
|
||||||
return
|
return
|
||||||
|
|
||||||
message = await self.services.chat.send(
|
message = await self.services.chat.send(
|
||||||
self.user_uid, channel_uid, message, is_final
|
self.user_uid, channel_uid, message, is_final
|
||||||
@ -328,7 +340,6 @@ class RPCView(BaseView):
|
|||||||
|
|
||||||
return message["uid"]
|
return message["uid"]
|
||||||
|
|
||||||
|
|
||||||
async def start_container(self, channel_uid):
|
async def start_container(self, channel_uid):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
channel_member = await self.services.channel_member.get(
|
channel_member = await self.services.channel_member.get(
|
||||||
@ -355,8 +366,6 @@ class RPCView(BaseView):
|
|||||||
if not channel_member:
|
if not channel_member:
|
||||||
raise Exception("Not allowed")
|
raise Exception("Not allowed")
|
||||||
return await self.services.container.get_status(channel_uid)
|
return await self.services.container.get_status(channel_uid)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def finalize_message(self, message_uid):
|
async def finalize_message(self, message_uid):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
@ -373,8 +382,8 @@ class RPCView(BaseView):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def update_message_text(self, message_uid, text):
|
async def update_message_text(self, message_uid, text):
|
||||||
|
|
||||||
#async with self.app.no_save():
|
# async with self.app.no_save():
|
||||||
self._require_login()
|
self._require_login()
|
||||||
|
|
||||||
message = await self.services.channel_message.get(message_uid)
|
message = await self.services.channel_message.get(message_uid)
|
||||||
@ -403,7 +412,6 @@ class RPCView(BaseView):
|
|||||||
"success": False,
|
"success": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if not text:
|
if not text:
|
||||||
message["deleted_at"] = now()
|
message["deleted_at"] = now()
|
||||||
else:
|
else:
|
||||||
@ -427,9 +435,6 @@ class RPCView(BaseView):
|
|||||||
|
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def clear_channel(self, channel_uid):
|
async def clear_channel(self, channel_uid):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
user = await self.services.user.get(uid=self.user_uid)
|
user = await self.services.user.get(uid=self.user_uid)
|
||||||
@ -438,11 +443,10 @@ class RPCView(BaseView):
|
|||||||
channel = await self.services.channel.get(uid=channel_uid)
|
channel = await self.services.channel.get(uid=channel_uid)
|
||||||
if not channel:
|
if not channel:
|
||||||
raise Exception("Channel not found")
|
raise Exception("Channel not found")
|
||||||
channel['history_start'] = datetime.now()
|
channel["history_start"] = datetime.now()
|
||||||
await self.services.channel.save(channel)
|
await self.services.channel.save(channel)
|
||||||
return await self.services.channel_message.clear(channel_uid)
|
return await self.services.channel_message.clear(channel_uid)
|
||||||
|
|
||||||
|
|
||||||
async def echo(self, *args):
|
async def echo(self, *args):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
return args
|
return args
|
||||||
@ -528,10 +532,10 @@ class RPCView(BaseView):
|
|||||||
async def _send_json(self, obj):
|
async def _send_json(self, obj):
|
||||||
try:
|
try:
|
||||||
await self.ws.send_str(json.dumps(obj, default=str))
|
await self.ws.send_str(json.dumps(obj, default=str))
|
||||||
except Exception as ex:
|
except Exception:
|
||||||
await self.services.socket.delete(self.ws)
|
await self.services.socket.delete(self.ws)
|
||||||
await self.ws.close()
|
await self.ws.close()
|
||||||
|
|
||||||
async def get_online_users(self, channel_uid):
|
async def get_online_users(self, channel_uid):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
|
|
||||||
|
|||||||
@ -38,10 +38,10 @@ class WebView(BaseView):
|
|||||||
channel = await self.services.channel.get(
|
channel = await self.services.channel.get(
|
||||||
uid=self.request.match_info.get("channel")
|
uid=self.request.match_info.get("channel")
|
||||||
)
|
)
|
||||||
print(self.session.get("uid"),"ZZZZZZZZZZ")
|
print(self.session.get("uid"), "ZZZZZZZZZZ")
|
||||||
qq = await self.services.user.get(uid=self.session.get("uid"))
|
qq = await self.services.user.get(uid=self.session.get("uid"))
|
||||||
|
|
||||||
print("GGGGGGGGGG",qq)
|
print("GGGGGGGGGG", qq)
|
||||||
if not channel:
|
if not channel:
|
||||||
user = await self.services.user.get(
|
user = await self.services.user.get(
|
||||||
uid=self.request.match_info.get("channel")
|
uid=self.request.match_info.get("channel")
|
||||||
|
|||||||
@ -1,22 +1,24 @@
|
|||||||
import logging
|
|
||||||
import pathlib
|
|
||||||
import base64
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import shutil
|
import shutil
|
||||||
import uuid
|
import uuid
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import aiohttp.web
|
import aiohttp.web
|
||||||
import hashlib
|
|
||||||
from app.cache import time_cache_async
|
from app.cache import time_cache_async
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
@aiohttp.web.middleware
|
@aiohttp.web.middleware
|
||||||
async def debug_middleware(request, handler):
|
async def debug_middleware(request, handler):
|
||||||
print(request.method, request.path, request.headers)
|
print(request.method, request.path, request.headers)
|
||||||
@ -28,6 +30,7 @@ async def debug_middleware(request, handler):
|
|||||||
pass
|
pass
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class WebdavApplication(aiohttp.web.Application):
|
class WebdavApplication(aiohttp.web.Application):
|
||||||
def __init__(self, parent, *args, **kwargs):
|
def __init__(self, parent, *args, **kwargs):
|
||||||
middlewares = [debug_middleware]
|
middlewares = [debug_middleware]
|
||||||
@ -78,7 +81,7 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
return request["user"]
|
return request["user"]
|
||||||
|
|
||||||
async def is_locked(self, abs_path, request):
|
async def is_locked(self, abs_path, request):
|
||||||
rel_path = abs_path.relative_to(request["home"]).as_posix()
|
abs_path.relative_to(request["home"]).as_posix()
|
||||||
path = abs_path
|
path = abs_path
|
||||||
while path != request["home"].parent:
|
while path != request["home"].parent:
|
||||||
rel = path.relative_to(request["home"]).as_posix()
|
rel = path.relative_to(request["home"]).as_posix()
|
||||||
@ -122,14 +125,20 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
return aiohttp.web.Response(status=403, text="Cannot download a directory")
|
return aiohttp.web.Response(status=403, text="Cannot download a directory")
|
||||||
lock = await self.is_locked(abs_path, request)
|
lock = await self.is_locked(abs_path, request)
|
||||||
if lock:
|
if lock:
|
||||||
submitted = request.headers.get("Lock-Token", "").strip(" <>").replace("opaquelocktoken:", "")
|
submitted = (
|
||||||
|
request.headers.get("Lock-Token", "")
|
||||||
|
.strip(" <>")
|
||||||
|
.replace("opaquelocktoken:", "")
|
||||||
|
)
|
||||||
if submitted != lock["token"]:
|
if submitted != lock["token"]:
|
||||||
return aiohttp.web.Response(status=423, text="Locked")
|
return aiohttp.web.Response(status=423, text="Locked")
|
||||||
content_type, _ = mimetypes.guess_type(str(abs_path))
|
content_type, _ = mimetypes.guess_type(str(abs_path))
|
||||||
content_type = content_type or "application/octet-stream"
|
content_type = content_type or "application/octet-stream"
|
||||||
etag = f'"{hashlib.sha1(str(abs_path.stat().st_mtime).encode()).hexdigest()}"'
|
etag = f'"{hashlib.sha1(str(abs_path.stat().st_mtime).encode()).hexdigest()}"'
|
||||||
headers = {"Content-Type": content_type, "ETag": etag}
|
headers = {"Content-Type": content_type, "ETag": etag}
|
||||||
return aiohttp.web.FileResponse(path=str(abs_path), headers=headers, chunk_size=8192)
|
return aiohttp.web.FileResponse(
|
||||||
|
path=str(abs_path), headers=headers, chunk_size=8192
|
||||||
|
)
|
||||||
|
|
||||||
async def handle_head(self, request):
|
async def handle_head(self, request):
|
||||||
if not await self.authenticate(request):
|
if not await self.authenticate(request):
|
||||||
@ -141,10 +150,16 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
if not abs_path.exists():
|
if not abs_path.exists():
|
||||||
return aiohttp.web.Response(status=404, text="File not found")
|
return aiohttp.web.Response(status=404, text="File not found")
|
||||||
if abs_path.is_dir():
|
if abs_path.is_dir():
|
||||||
return aiohttp.web.Response(status=403, text="Cannot get metadata for a directory")
|
return aiohttp.web.Response(
|
||||||
|
status=403, text="Cannot get metadata for a directory"
|
||||||
|
)
|
||||||
lock = await self.is_locked(abs_path, request)
|
lock = await self.is_locked(abs_path, request)
|
||||||
if lock:
|
if lock:
|
||||||
submitted = request.headers.get("Lock-Token", "").strip(" <>").replace("opaquelocktoken:", "")
|
submitted = (
|
||||||
|
request.headers.get("Lock-Token", "")
|
||||||
|
.strip(" <>")
|
||||||
|
.replace("opaquelocktoken:", "")
|
||||||
|
)
|
||||||
if submitted != lock["token"]:
|
if submitted != lock["token"]:
|
||||||
return aiohttp.web.Response(status=423, text="Locked")
|
return aiohttp.web.Response(status=423, text="Locked")
|
||||||
content_type, _ = mimetypes.guess_type(str(abs_path))
|
content_type, _ = mimetypes.guess_type(str(abs_path))
|
||||||
@ -168,7 +183,11 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
abs_path = request["home"] / requested_path
|
abs_path = request["home"] / requested_path
|
||||||
lock = await self.is_locked(abs_path, request)
|
lock = await self.is_locked(abs_path, request)
|
||||||
if lock:
|
if lock:
|
||||||
submitted = request.headers.get("Lock-Token", "").strip(" <>").replace("opaquelocktoken:", "")
|
submitted = (
|
||||||
|
request.headers.get("Lock-Token", "")
|
||||||
|
.strip(" <>")
|
||||||
|
.replace("opaquelocktoken:", "")
|
||||||
|
)
|
||||||
if submitted != lock["token"]:
|
if submitted != lock["token"]:
|
||||||
return aiohttp.web.Response(status=423, text="Locked")
|
return aiohttp.web.Response(status=423, text="Locked")
|
||||||
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@ -186,7 +205,11 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
abs_path = request["home"] / requested_path
|
abs_path = request["home"] / requested_path
|
||||||
lock = await self.is_locked(abs_path, request)
|
lock = await self.is_locked(abs_path, request)
|
||||||
if lock:
|
if lock:
|
||||||
submitted = request.headers.get("Lock-Token", "").strip(" <>").replace("opaquelocktoken:", "")
|
submitted = (
|
||||||
|
request.headers.get("Lock-Token", "")
|
||||||
|
.strip(" <>")
|
||||||
|
.replace("opaquelocktoken:", "")
|
||||||
|
)
|
||||||
if submitted != lock["token"]:
|
if submitted != lock["token"]:
|
||||||
return aiohttp.web.Response(status=423, text="Locked")
|
return aiohttp.web.Response(status=423, text="Locked")
|
||||||
if abs_path.is_file():
|
if abs_path.is_file():
|
||||||
@ -199,7 +222,9 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
del self.locks[rel_path]
|
del self.locks[rel_path]
|
||||||
return aiohttp.web.Response(status=204)
|
return aiohttp.web.Response(status=204)
|
||||||
elif abs_path.is_dir():
|
elif abs_path.is_dir():
|
||||||
if await self.has_descendant_locks(abs_path.relative_to(request["home"]).as_posix(), True):
|
if await self.has_descendant_locks(
|
||||||
|
abs_path.relative_to(request["home"]).as_posix(), True
|
||||||
|
):
|
||||||
return aiohttp.web.Response(status=423, text="Locked")
|
return aiohttp.web.Response(status=423, text="Locked")
|
||||||
shutil.rmtree(abs_path)
|
shutil.rmtree(abs_path)
|
||||||
rel_path = abs_path.relative_to(request["home"]).as_posix()
|
rel_path = abs_path.relative_to(request["home"]).as_posix()
|
||||||
@ -217,7 +242,11 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
abs_path = request["home"] / requested_path
|
abs_path = request["home"] / requested_path
|
||||||
lock = await self.is_locked(abs_path, request)
|
lock = await self.is_locked(abs_path, request)
|
||||||
if lock:
|
if lock:
|
||||||
submitted = request.headers.get("Lock-Token", "").strip(" <>").replace("opaquelocktoken:", "")
|
submitted = (
|
||||||
|
request.headers.get("Lock-Token", "")
|
||||||
|
.strip(" <>")
|
||||||
|
.replace("opaquelocktoken:", "")
|
||||||
|
)
|
||||||
if submitted != lock["token"]:
|
if submitted != lock["token"]:
|
||||||
return aiohttp.web.Response(status=423, text="Locked")
|
return aiohttp.web.Response(status=423, text="Locked")
|
||||||
if abs_path.exists():
|
if abs_path.exists():
|
||||||
@ -235,15 +264,25 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
if not destination:
|
if not destination:
|
||||||
return aiohttp.web.Response(status=400, text="Destination header missing")
|
return aiohttp.web.Response(status=400, text="Destination header missing")
|
||||||
parsed = urlparse(destination)
|
parsed = urlparse(destination)
|
||||||
if parsed.scheme and (parsed.scheme != request.scheme or parsed.netloc != request.host):
|
if parsed.scheme and (
|
||||||
|
parsed.scheme != request.scheme or parsed.netloc != request.host
|
||||||
|
):
|
||||||
return aiohttp.web.Response(status=502, text="Bad Gateway")
|
return aiohttp.web.Response(status=502, text="Bad Gateway")
|
||||||
dest_rel = parsed.path[len(self.relative_url)+1:] if parsed.path.startswith(self.relative_url) else ""
|
dest_rel = (
|
||||||
|
parsed.path[len(self.relative_url) + 1 :]
|
||||||
|
if parsed.path.startswith(self.relative_url)
|
||||||
|
else ""
|
||||||
|
)
|
||||||
dest_path = request["home"] / dest_rel
|
dest_path = request["home"] / dest_rel
|
||||||
if not src_path.exists():
|
if not src_path.exists():
|
||||||
return aiohttp.web.Response(status=404, text="Source not found")
|
return aiohttp.web.Response(status=404, text="Source not found")
|
||||||
src_lock = await self.is_locked(src_path, request)
|
src_lock = await self.is_locked(src_path, request)
|
||||||
dest_lock = await self.is_locked(dest_path, request)
|
dest_lock = await self.is_locked(dest_path, request)
|
||||||
submitted = request.headers.get("Lock-Token", "").strip(" <>").replace("opaquelocktoken:", "")
|
submitted = (
|
||||||
|
request.headers.get("Lock-Token", "")
|
||||||
|
.strip(" <>")
|
||||||
|
.replace("opaquelocktoken:", "")
|
||||||
|
)
|
||||||
if src_lock and src_lock["token"] != submitted:
|
if src_lock and src_lock["token"] != submitted:
|
||||||
return aiohttp.web.Response(status=423, text="Source Locked")
|
return aiohttp.web.Response(status=423, text="Source Locked")
|
||||||
if dest_lock and dest_lock["token"] != submitted:
|
if dest_lock and dest_lock["token"] != submitted:
|
||||||
@ -277,15 +316,25 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
if not destination:
|
if not destination:
|
||||||
return aiohttp.web.Response(status=400, text="Destination header missing")
|
return aiohttp.web.Response(status=400, text="Destination header missing")
|
||||||
parsed = urlparse(destination)
|
parsed = urlparse(destination)
|
||||||
if parsed.scheme and (parsed.scheme != request.scheme or parsed.netloc != request.host):
|
if parsed.scheme and (
|
||||||
|
parsed.scheme != request.scheme or parsed.netloc != request.host
|
||||||
|
):
|
||||||
return aiohttp.web.Response(status=502, text="Bad Gateway")
|
return aiohttp.web.Response(status=502, text="Bad Gateway")
|
||||||
dest_rel = parsed.path[len(self.relative_url)+1:] if parsed.path.startswith(self.relative_url) else ""
|
dest_rel = (
|
||||||
|
parsed.path[len(self.relative_url) + 1 :]
|
||||||
|
if parsed.path.startswith(self.relative_url)
|
||||||
|
else ""
|
||||||
|
)
|
||||||
dest_path = request["home"] / dest_rel
|
dest_path = request["home"] / dest_rel
|
||||||
if not src_path.exists():
|
if not src_path.exists():
|
||||||
return aiohttp.web.Response(status=404, text="Source not found")
|
return aiohttp.web.Response(status=404, text="Source not found")
|
||||||
src_lock = await self.is_locked(src_path, request)
|
src_lock = await self.is_locked(src_path, request)
|
||||||
dest_lock = await self.is_locked(dest_path, request)
|
dest_lock = await self.is_locked(dest_path, request)
|
||||||
submitted = request.headers.get("Lock-Token", "").strip(" <>").replace("opaquelocktoken:", "")
|
submitted = (
|
||||||
|
request.headers.get("Lock-Token", "")
|
||||||
|
.strip(" <>")
|
||||||
|
.replace("opaquelocktoken:", "")
|
||||||
|
)
|
||||||
if src_lock and src_lock["token"] != submitted:
|
if src_lock and src_lock["token"] != submitted:
|
||||||
return aiohttp.web.Response(status=423, text="Source Locked")
|
return aiohttp.web.Response(status=423, text="Source Locked")
|
||||||
if dest_lock and dest_lock["token"] != submitted:
|
if dest_lock and dest_lock["token"] != submitted:
|
||||||
@ -347,7 +396,16 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
statvfs = await loop.run_in_executor(None, os.statvfs, path)
|
statvfs = await loop.run_in_executor(None, os.statvfs, path)
|
||||||
return statvfs.f_bavail * statvfs.f_frsize
|
return statvfs.f_bavail * statvfs.f_frsize
|
||||||
|
|
||||||
async def create_propfind_node(self, request, response_xml, full_path, depth, requested_props, propname_only, all_props):
|
async def create_propfind_node(
|
||||||
|
self,
|
||||||
|
request,
|
||||||
|
response_xml,
|
||||||
|
full_path,
|
||||||
|
depth,
|
||||||
|
requested_props,
|
||||||
|
propname_only,
|
||||||
|
all_props,
|
||||||
|
):
|
||||||
abs_path = pathlib.Path(full_path)
|
abs_path = pathlib.Path(full_path)
|
||||||
relative_path = abs_path.relative_to(request["home"]).as_posix()
|
relative_path = abs_path.relative_to(request["home"]).as_posix()
|
||||||
href_path = f"{self.relative_url}/{relative_path}".strip(".")
|
href_path = f"{self.relative_url}/{relative_path}".strip(".")
|
||||||
@ -379,7 +437,9 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
prop_notfound = None
|
prop_notfound = None
|
||||||
for prop_tag, prop_nsmap in requested_props:
|
for prop_tag, prop_nsmap in requested_props:
|
||||||
if prop_tag in standard_props:
|
if prop_tag in standard_props:
|
||||||
await self.add_standard_property(prop_ok, prop_tag, full_path, request)
|
await self.add_standard_property(
|
||||||
|
prop_ok, prop_tag, full_path, request
|
||||||
|
)
|
||||||
found_props.append(prop_tag)
|
found_props.append(prop_tag)
|
||||||
elif prop_tag in custom_properties:
|
elif prop_tag in custom_properties:
|
||||||
if propname_only:
|
if propname_only:
|
||||||
@ -391,23 +451,31 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
else:
|
else:
|
||||||
if propstat_notfound is None:
|
if propstat_notfound is None:
|
||||||
propstat_notfound = etree.SubElement(response, "{DAV:}propstat")
|
propstat_notfound = etree.SubElement(response, "{DAV:}propstat")
|
||||||
prop_notfound = etree.SubElement(propstat_notfound, "{DAV:}prop")
|
prop_notfound = etree.SubElement(
|
||||||
|
propstat_notfound, "{DAV:}prop"
|
||||||
|
)
|
||||||
etree.SubElement(prop_notfound, prop_tag, nsmap=prop_nsmap)
|
etree.SubElement(prop_notfound, prop_tag, nsmap=prop_nsmap)
|
||||||
if propstat_notfound is not None:
|
if propstat_notfound is not None:
|
||||||
etree.SubElement(propstat_notfound, "{DAV:}status").text = "HTTP/1.1 404 Not Found"
|
etree.SubElement(propstat_notfound, "{DAV:}status").text = (
|
||||||
|
"HTTP/1.1 404 Not Found"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
for prop_name in standard_props:
|
for prop_name in standard_props:
|
||||||
if propname_only:
|
if propname_only:
|
||||||
etree.SubElement(prop_ok, prop_name)
|
etree.SubElement(prop_ok, prop_name)
|
||||||
else:
|
else:
|
||||||
await self.add_standard_property(prop_ok, prop_name, full_path, request)
|
await self.add_standard_property(
|
||||||
|
prop_ok, prop_name, full_path, request
|
||||||
|
)
|
||||||
for prop_name, prop_value in custom_properties.items():
|
for prop_name, prop_value in custom_properties.items():
|
||||||
if propname_only:
|
if propname_only:
|
||||||
if prop_name.startswith("{"):
|
if prop_name.startswith("{"):
|
||||||
ns_end = prop_name.find("}")
|
ns_end = prop_name.find("}")
|
||||||
ns = prop_name[1:ns_end]
|
ns = prop_name[1:ns_end]
|
||||||
local_name = prop_name[ns_end+1:]
|
local_name = prop_name[ns_end + 1 :]
|
||||||
prop_nsmap = {"D": "DAV:", None: ns} if ns != "DAV:" else {"D": "DAV:"}
|
prop_nsmap = (
|
||||||
|
{"D": "DAV:", None: ns} if ns != "DAV:" else {"D": "DAV:"}
|
||||||
|
)
|
||||||
etree.SubElement(prop_ok, local_name, nsmap=prop_nsmap)
|
etree.SubElement(prop_ok, local_name, nsmap=prop_nsmap)
|
||||||
else:
|
else:
|
||||||
etree.SubElement(prop_ok, prop_name)
|
etree.SubElement(prop_ok, prop_name)
|
||||||
@ -417,9 +485,19 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
etree.SubElement(propstat_ok, "{DAV:}status").text = "HTTP/1.1 200 OK"
|
etree.SubElement(propstat_ok, "{DAV:}status").text = "HTTP/1.1 200 OK"
|
||||||
if abs_path.is_dir() and depth > 0:
|
if abs_path.is_dir() and depth > 0:
|
||||||
for item in abs_path.iterdir():
|
for item in abs_path.iterdir():
|
||||||
if item.name.startswith(".") and item.name.endswith(".webdav_props.json"):
|
if item.name.startswith(".") and item.name.endswith(
|
||||||
|
".webdav_props.json"
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
await self.create_propfind_node(request, response_xml, item, depth - 1, requested_props, propname_only, all_props)
|
await self.create_propfind_node(
|
||||||
|
request,
|
||||||
|
response_xml,
|
||||||
|
item,
|
||||||
|
depth - 1,
|
||||||
|
requested_props,
|
||||||
|
propname_only,
|
||||||
|
all_props,
|
||||||
|
)
|
||||||
|
|
||||||
async def add_standard_property(self, prop_elem, prop_name, full_path, request):
|
async def add_standard_property(self, prop_elem, prop_name, full_path, request):
|
||||||
if prop_name == "{DAV:}resourcetype":
|
if prop_name == "{DAV:}resourcetype":
|
||||||
@ -445,14 +523,22 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
etree.SubElement(locktype, f"{{DAV:}}{lock['type']}")
|
etree.SubElement(locktype, f"{{DAV:}}{lock['type']}")
|
||||||
lockscope = etree.SubElement(activelock, "{DAV:}lockscope")
|
lockscope = etree.SubElement(activelock, "{DAV:}lockscope")
|
||||||
etree.SubElement(lockscope, f"{{DAV:}}{lock['scope']}")
|
etree.SubElement(lockscope, f"{{DAV:}}{lock['scope']}")
|
||||||
etree.SubElement(activelock, "{DAV:}depth").text = lock['depth'].capitalize()
|
etree.SubElement(activelock, "{DAV:}depth").text = lock[
|
||||||
if lock['owner']:
|
"depth"
|
||||||
owner = etree.fromstring(lock['owner'])
|
].capitalize()
|
||||||
|
if lock["owner"]:
|
||||||
|
owner = etree.fromstring(lock["owner"])
|
||||||
activelock.append(owner)
|
activelock.append(owner)
|
||||||
timeout_str = "Infinite" if lock['timeout'] is None else f"Second-{lock['timeout']}"
|
timeout_str = (
|
||||||
|
"Infinite"
|
||||||
|
if lock["timeout"] is None
|
||||||
|
else f"Second-{lock['timeout']}"
|
||||||
|
)
|
||||||
etree.SubElement(activelock, "{DAV:}timeout").text = timeout_str
|
etree.SubElement(activelock, "{DAV:}timeout").text = timeout_str
|
||||||
locktoken = etree.SubElement(activelock, "{DAV:}locktoken")
|
locktoken = etree.SubElement(activelock, "{DAV:}locktoken")
|
||||||
etree.SubElement(locktoken, "{DAV:}href").text = f"opaquelocktoken:{lock['token']}"
|
etree.SubElement(locktoken, "{DAV:}href").text = (
|
||||||
|
f"opaquelocktoken:{lock['token']}"
|
||||||
|
)
|
||||||
elif prop_name == "{DAV:}supportedlock":
|
elif prop_name == "{DAV:}supportedlock":
|
||||||
supported_lock = etree.SubElement(prop_elem, "{DAV:}supportedlock")
|
supported_lock = etree.SubElement(prop_elem, "{DAV:}supportedlock")
|
||||||
lock_entry_1 = etree.SubElement(supported_lock, "{DAV:}lockentry")
|
lock_entry_1 = etree.SubElement(supported_lock, "{DAV:}lockentry")
|
||||||
@ -466,11 +552,17 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
lock_type_2 = etree.SubElement(lock_entry_2, "{DAV:}locktype")
|
lock_type_2 = etree.SubElement(lock_entry_2, "{DAV:}locktype")
|
||||||
etree.SubElement(lock_type_2, "{DAV:}write")
|
etree.SubElement(lock_type_2, "{DAV:}write")
|
||||||
elif prop_name == "{DAV:}quota-used-bytes":
|
elif prop_name == "{DAV:}quota-used-bytes":
|
||||||
size = await self.get_file_size(full_path) if full_path.is_file() else await self.get_directory_size(full_path)
|
size = (
|
||||||
|
await self.get_file_size(full_path)
|
||||||
|
if full_path.is_file()
|
||||||
|
else await self.get_directory_size(full_path)
|
||||||
|
)
|
||||||
etree.SubElement(prop_elem, "{DAV:}quota-used-bytes").text = str(size)
|
etree.SubElement(prop_elem, "{DAV:}quota-used-bytes").text = str(size)
|
||||||
elif prop_name == "{DAV:}quota-available-bytes":
|
elif prop_name == "{DAV:}quota-available-bytes":
|
||||||
free_space = await self.get_disk_free_space(str(full_path))
|
free_space = await self.get_disk_free_space(str(full_path))
|
||||||
etree.SubElement(prop_elem, "{DAV:}quota-available-bytes").text = str(free_space)
|
etree.SubElement(prop_elem, "{DAV:}quota-available-bytes").text = str(
|
||||||
|
free_space
|
||||||
|
)
|
||||||
elif prop_name == "{DAV:}getcontentlength" and full_path.is_file():
|
elif prop_name == "{DAV:}getcontentlength" and full_path.is_file():
|
||||||
size = await self.get_file_size(full_path)
|
size = await self.get_file_size(full_path)
|
||||||
etree.SubElement(prop_elem, "{DAV:}getcontentlength").text = str(size)
|
etree.SubElement(prop_elem, "{DAV:}getcontentlength").text = str(size)
|
||||||
@ -479,7 +571,9 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
if mimetype:
|
if mimetype:
|
||||||
etree.SubElement(prop_elem, "{DAV:}getcontenttype").text = mimetype
|
etree.SubElement(prop_elem, "{DAV:}getcontenttype").text = mimetype
|
||||||
elif prop_name == "{DAV:}getetag" and full_path.is_file():
|
elif prop_name == "{DAV:}getetag" and full_path.is_file():
|
||||||
etag = f'"{hashlib.sha1(str(full_path.stat().st_mtime).encode()).hexdigest()}"'
|
etag = (
|
||||||
|
f'"{hashlib.sha1(str(full_path.stat().st_mtime).encode()).hexdigest()}"'
|
||||||
|
)
|
||||||
etree.SubElement(prop_elem, "{DAV:}getetag").text = etag
|
etree.SubElement(prop_elem, "{DAV:}getetag").text = etag
|
||||||
|
|
||||||
async def handle_propfind(self, request):
|
async def handle_propfind(self, request):
|
||||||
@ -520,9 +614,21 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
pass
|
pass
|
||||||
nsmap = {"D": "DAV:"}
|
nsmap = {"D": "DAV:"}
|
||||||
response_xml = etree.Element("{DAV:}multistatus", nsmap=nsmap)
|
response_xml = etree.Element("{DAV:}multistatus", nsmap=nsmap)
|
||||||
await self.create_propfind_node(request, response_xml, abs_path, depth, requested_props, propname_only, all_props)
|
await self.create_propfind_node(
|
||||||
xml_output = etree.tostring(response_xml, encoding="utf-8", xml_declaration=True).decode()
|
request,
|
||||||
return aiohttp.web.Response(status=207, text=xml_output, content_type="application/xml")
|
response_xml,
|
||||||
|
abs_path,
|
||||||
|
depth,
|
||||||
|
requested_props,
|
||||||
|
propname_only,
|
||||||
|
all_props,
|
||||||
|
)
|
||||||
|
xml_output = etree.tostring(
|
||||||
|
response_xml, encoding="utf-8", xml_declaration=True
|
||||||
|
).decode()
|
||||||
|
return aiohttp.web.Response(
|
||||||
|
status=207, text=xml_output, content_type="application/xml"
|
||||||
|
)
|
||||||
|
|
||||||
async def handle_proppatch(self, request):
|
async def handle_proppatch(self, request):
|
||||||
if not await self.authenticate(request):
|
if not await self.authenticate(request):
|
||||||
@ -535,7 +641,11 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
return aiohttp.web.Response(status=404, text="Resource not found")
|
return aiohttp.web.Response(status=404, text="Resource not found")
|
||||||
lock = await self.is_locked(abs_path, request)
|
lock = await self.is_locked(abs_path, request)
|
||||||
if lock:
|
if lock:
|
||||||
submitted = request.headers.get("Lock-Token", "").strip(" <>").replace("opaquelocktoken:", "")
|
submitted = (
|
||||||
|
request.headers.get("Lock-Token", "")
|
||||||
|
.strip(" <>")
|
||||||
|
.replace("opaquelocktoken:", "")
|
||||||
|
)
|
||||||
if submitted != lock["token"]:
|
if submitted != lock["token"]:
|
||||||
return aiohttp.web.Response(status=423, text="Locked")
|
return aiohttp.web.Response(status=423, text="Locked")
|
||||||
body = await request.read()
|
body = await request.read()
|
||||||
@ -549,7 +659,11 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
response = etree.SubElement(response_xml, "{DAV:}response")
|
response = etree.SubElement(response_xml, "{DAV:}response")
|
||||||
href = etree.SubElement(response, "{DAV:}href")
|
href = etree.SubElement(response, "{DAV:}href")
|
||||||
relative_path = abs_path.relative_to(request["home"]).as_posix()
|
relative_path = abs_path.relative_to(request["home"]).as_posix()
|
||||||
href_path = f"{self.relative_url}/{relative_path}".strip(".").replace("./", "/").replace("//", "/")
|
href_path = (
|
||||||
|
f"{self.relative_url}/{relative_path}".strip(".")
|
||||||
|
.replace("./", "/")
|
||||||
|
.replace("//", "/")
|
||||||
|
)
|
||||||
href.text = href_path
|
href.text = href_path
|
||||||
set_elem = root.find(".//{DAV:}set")
|
set_elem = root.find(".//{DAV:}set")
|
||||||
if set_elem is not None:
|
if set_elem is not None:
|
||||||
@ -561,14 +675,14 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
if child.tag.startswith("{"):
|
if child.tag.startswith("{"):
|
||||||
ns_end = child.tag.find("}")
|
ns_end = child.tag.find("}")
|
||||||
ns = child.tag[1:ns_end]
|
ns = child.tag[1:ns_end]
|
||||||
local_name = child.tag[ns_end+1:]
|
local_name = child.tag[ns_end + 1 :]
|
||||||
else:
|
else:
|
||||||
ns = "DAV:"
|
ns = "DAV:"
|
||||||
local_name = child.tag
|
local_name = child.tag
|
||||||
prop_name = f"{{{ns}}}{local_name}"
|
prop_name = f"{{{ns}}}{local_name}"
|
||||||
prop_value = child.text or ""
|
prop_value = child.text or ""
|
||||||
properties[prop_name] = prop_value
|
properties[prop_name] = prop_value
|
||||||
elem = etree.SubElement(prop, child.tag, nsmap=child.nsmap)
|
etree.SubElement(prop, child.tag, nsmap=child.nsmap)
|
||||||
etree.SubElement(propstat, "{DAV:}status").text = "HTTP/1.1 200 OK"
|
etree.SubElement(propstat, "{DAV:}status").text = "HTTP/1.1 200 OK"
|
||||||
remove_elem = root.find(".//{DAV:}remove")
|
remove_elem = root.find(".//{DAV:}remove")
|
||||||
if remove_elem is not None:
|
if remove_elem is not None:
|
||||||
@ -580,7 +694,7 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
if child.tag.startswith("{"):
|
if child.tag.startswith("{"):
|
||||||
ns_end = child.tag.find("}")
|
ns_end = child.tag.find("}")
|
||||||
ns = child.tag[1:ns_end]
|
ns = child.tag[1:ns_end]
|
||||||
local_name = child.tag[ns_end+1:]
|
local_name = child.tag[ns_end + 1 :]
|
||||||
else:
|
else:
|
||||||
ns = "DAV:"
|
ns = "DAV:"
|
||||||
local_name = child.tag
|
local_name = child.tag
|
||||||
@ -590,14 +704,18 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
status = "HTTP/1.1 200 OK"
|
status = "HTTP/1.1 200 OK"
|
||||||
else:
|
else:
|
||||||
status = "HTTP/1.1 404 Not Found"
|
status = "HTTP/1.1 404 Not Found"
|
||||||
elem = etree.SubElement(prop, child.tag, nsmap=child.nsmap)
|
etree.SubElement(prop, child.tag, nsmap=child.nsmap)
|
||||||
etree.SubElement(propstat, "{DAV:}status").text = status
|
etree.SubElement(propstat, "{DAV:}status").text = status
|
||||||
if properties:
|
if properties:
|
||||||
await self.save_properties(abs_path, properties)
|
await self.save_properties(abs_path, properties)
|
||||||
else:
|
else:
|
||||||
await self.delete_properties_file(abs_path)
|
await self.delete_properties_file(abs_path)
|
||||||
xml_output = etree.tostring(response_xml, encoding="utf-8", xml_declaration=True).decode()
|
xml_output = etree.tostring(
|
||||||
return aiohttp.web.Response(status=207, text=xml_output, content_type="application/xml")
|
response_xml, encoding="utf-8", xml_declaration=True
|
||||||
|
).decode()
|
||||||
|
return aiohttp.web.Response(
|
||||||
|
status=207, text=xml_output, content_type="application/xml"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"PROPPATCH error: {e}")
|
logging.error(f"PROPPATCH error: {e}")
|
||||||
return aiohttp.web.Response(status=400, text="Bad Request")
|
return aiohttp.web.Response(status=400, text="Bad Request")
|
||||||
@ -639,11 +757,19 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
scope = "shared"
|
scope = "shared"
|
||||||
locktype = "write"
|
locktype = "write"
|
||||||
owner_elem = lockinfo.find("{DAV:}owner")
|
owner_elem = lockinfo.find("{DAV:}owner")
|
||||||
owner_xml = etree.tostring(owner_elem, encoding="unicode") if owner_elem is not None else ""
|
owner_xml = (
|
||||||
|
etree.tostring(owner_elem, encoding="unicode")
|
||||||
|
if owner_elem is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
covering_lock = await self.is_locked(abs_path, request)
|
covering_lock = await self.is_locked(abs_path, request)
|
||||||
if covering_lock:
|
if covering_lock:
|
||||||
return aiohttp.web.Response(status=423, text="Locked")
|
return aiohttp.web.Response(status=423, text="Locked")
|
||||||
if abs_path.is_dir() and depth == "infinity" and await self.has_descendant_locks(resource, True):
|
if (
|
||||||
|
abs_path.is_dir()
|
||||||
|
and depth == "infinity"
|
||||||
|
and await self.has_descendant_locks(resource, True)
|
||||||
|
):
|
||||||
return aiohttp.web.Response(status=423, text="Locked")
|
return aiohttp.web.Response(status=423, text="Locked")
|
||||||
token = str(uuid.uuid4())
|
token = str(uuid.uuid4())
|
||||||
lock_dict = {
|
lock_dict = {
|
||||||
@ -653,12 +779,17 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
"owner": owner_xml,
|
"owner": owner_xml,
|
||||||
"depth": depth,
|
"depth": depth,
|
||||||
"timeout": timeout,
|
"timeout": timeout,
|
||||||
"created": datetime.datetime.utcnow()
|
"created": datetime.datetime.utcnow(),
|
||||||
}
|
}
|
||||||
self.locks[resource] = lock_dict
|
self.locks[resource] = lock_dict
|
||||||
xml = await self.generate_lock_response(lock_dict)
|
xml = await self.generate_lock_response(lock_dict)
|
||||||
headers = {"Lock-Token": f"<opaquelocktoken:{token}>"}
|
headers = {"Lock-Token": f"<opaquelocktoken:{token}>"}
|
||||||
return aiohttp.web.Response(status=200, text=xml, content_type="application/xml", headers=headers)
|
return aiohttp.web.Response(
|
||||||
|
status=200,
|
||||||
|
text=xml,
|
||||||
|
content_type="application/xml",
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"LOCK error: {e}")
|
logging.error(f"LOCK error: {e}")
|
||||||
return aiohttp.web.Response(status=400, text="Bad Request")
|
return aiohttp.web.Response(status=400, text="Bad Request")
|
||||||
@ -669,14 +800,20 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
if self.is_lock_expired(lock):
|
if self.is_lock_expired(lock):
|
||||||
del self.locks[resource]
|
del self.locks[resource]
|
||||||
return aiohttp.web.Response(status=412, text="Precondition Failed")
|
return aiohttp.web.Response(status=412, text="Precondition Failed")
|
||||||
submitted = request.headers.get("Lock-Token", "").strip(" <>").replace("opaquelocktoken:", "")
|
submitted = (
|
||||||
|
request.headers.get("Lock-Token", "")
|
||||||
|
.strip(" <>")
|
||||||
|
.replace("opaquelocktoken:", "")
|
||||||
|
)
|
||||||
if submitted != lock["token"]:
|
if submitted != lock["token"]:
|
||||||
return aiohttp.web.Response(status=409, text="Conflict")
|
return aiohttp.web.Response(status=409, text="Conflict")
|
||||||
lock["created"] = datetime.datetime.utcnow()
|
lock["created"] = datetime.datetime.utcnow()
|
||||||
lock["timeout"] = timeout
|
lock["timeout"] = timeout
|
||||||
xml = await self.generate_lock_response(lock)
|
xml = await self.generate_lock_response(lock)
|
||||||
headers = {"Lock-Token": f"<opaquelocktoken:{lock['token']}>"}
|
headers = {"Lock-Token": f"<opaquelocktoken:{lock['token']}>"}
|
||||||
return aiohttp.web.Response(status=200, text=xml, content_type="application/xml", headers=headers)
|
return aiohttp.web.Response(
|
||||||
|
status=200, text=xml, content_type="application/xml", headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
async def handle_unlock(self, request):
|
async def handle_unlock(self, request):
|
||||||
if not await self.authenticate(request):
|
if not await self.authenticate(request):
|
||||||
@ -690,7 +827,11 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
if self.is_lock_expired(lock):
|
if self.is_lock_expired(lock):
|
||||||
del self.locks[resource]
|
del self.locks[resource]
|
||||||
return aiohttp.web.Response(status=409, text="Conflict")
|
return aiohttp.web.Response(status=409, text="Conflict")
|
||||||
submitted = request.headers.get("Lock-Token", "").strip(" <>").replace("opaquelocktoken:", "")
|
submitted = (
|
||||||
|
request.headers.get("Lock-Token", "")
|
||||||
|
.strip(" <>")
|
||||||
|
.replace("opaquelocktoken:", "")
|
||||||
|
)
|
||||||
if submitted != lock["token"]:
|
if submitted != lock["token"]:
|
||||||
return aiohttp.web.Response(status=400, text="Invalid Lock Token")
|
return aiohttp.web.Response(status=400, text="Invalid Lock Token")
|
||||||
del self.locks[resource]
|
del self.locks[resource]
|
||||||
@ -705,14 +846,22 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
etree.SubElement(locktype, f"{{DAV:}}{lock_dict['type']}")
|
etree.SubElement(locktype, f"{{DAV:}}{lock_dict['type']}")
|
||||||
lockscope = etree.SubElement(activelock, "{DAV:}lockscope")
|
lockscope = etree.SubElement(activelock, "{DAV:}lockscope")
|
||||||
etree.SubElement(lockscope, f"{{DAV:}}{lock_dict['scope']}")
|
etree.SubElement(lockscope, f"{{DAV:}}{lock_dict['scope']}")
|
||||||
etree.SubElement(activelock, "{DAV:}depth").text = lock_dict['depth'].capitalize()
|
etree.SubElement(activelock, "{DAV:}depth").text = lock_dict[
|
||||||
if lock_dict['owner']:
|
"depth"
|
||||||
owner = etree.fromstring(lock_dict['owner'])
|
].capitalize()
|
||||||
|
if lock_dict["owner"]:
|
||||||
|
owner = etree.fromstring(lock_dict["owner"])
|
||||||
activelock.append(owner)
|
activelock.append(owner)
|
||||||
timeout_str = "Infinite" if lock_dict['timeout'] is None else f"Second-{lock_dict['timeout']}"
|
timeout_str = (
|
||||||
|
"Infinite"
|
||||||
|
if lock_dict["timeout"] is None
|
||||||
|
else f"Second-{lock_dict['timeout']}"
|
||||||
|
)
|
||||||
etree.SubElement(activelock, "{DAV:}timeout").text = timeout_str
|
etree.SubElement(activelock, "{DAV:}timeout").text = timeout_str
|
||||||
locktoken = etree.SubElement(activelock, "{DAV:}locktoken")
|
locktoken = etree.SubElement(activelock, "{DAV:}locktoken")
|
||||||
etree.SubElement(locktoken, "{DAV:}href").text = f"opaquelocktoken:{lock_dict['token']}"
|
etree.SubElement(locktoken, "{DAV:}href").text = (
|
||||||
|
f"opaquelocktoken:{lock_dict['token']}"
|
||||||
|
)
|
||||||
return etree.tostring(prop, encoding="utf-8", xml_declaration=True).decode()
|
return etree.tostring(prop, encoding="utf-8", xml_declaration=True).decode()
|
||||||
|
|
||||||
def get_last_modified(self, path):
|
def get_last_modified(self, path):
|
||||||
@ -731,7 +880,7 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
async def load_properties(self, resource_path):
|
async def load_properties(self, resource_path):
|
||||||
props_file = self.get_props_file_path(resource_path)
|
props_file = self.get_props_file_path(resource_path)
|
||||||
if props_file.exists():
|
if props_file.exists():
|
||||||
async with aiofiles.open(props_file, "r") as f:
|
async with aiofiles.open(props_file) as f:
|
||||||
content = await f.read()
|
content = await f.read()
|
||||||
return json.loads(content)
|
return json.loads(content)
|
||||||
return {}
|
return {}
|
||||||
@ -773,7 +922,11 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
response = etree.SubElement(response_xml, "{DAV:}response")
|
response = etree.SubElement(response_xml, "{DAV:}response")
|
||||||
href = etree.SubElement(response, "{DAV:}href")
|
href = etree.SubElement(response, "{DAV:}href")
|
||||||
relative_path = abs_path.relative_to(request["home"]).as_posix()
|
relative_path = abs_path.relative_to(request["home"]).as_posix()
|
||||||
href_path = f"{self.relative_url}/{relative_path}".strip(".").replace("./", "/").replace("//", "/")
|
href_path = (
|
||||||
|
f"{self.relative_url}/{relative_path}".strip(".")
|
||||||
|
.replace("./", "/")
|
||||||
|
.replace("//", "/")
|
||||||
|
)
|
||||||
href.text = href_path
|
href.text = href_path
|
||||||
propstat = etree.SubElement(response, "{DAV:}propstat")
|
propstat = etree.SubElement(response, "{DAV:}propstat")
|
||||||
prop = etree.SubElement(propstat, "{DAV:}prop")
|
prop = etree.SubElement(propstat, "{DAV:}prop")
|
||||||
@ -787,8 +940,12 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
elem = etree.SubElement(prop, prop_name)
|
elem = etree.SubElement(prop, prop_name)
|
||||||
elem.text = str(prop_value)
|
elem.text = str(prop_value)
|
||||||
etree.SubElement(propstat, "{DAV:}status").text = "HTTP/1.1 200 OK"
|
etree.SubElement(propstat, "{DAV:}status").text = "HTTP/1.1 200 OK"
|
||||||
xml_output = etree.tostring(response_xml, encoding="utf-8", xml_declaration=True).decode()
|
xml_output = etree.tostring(
|
||||||
return aiohttp.web.Response(status=207, text=xml_output, content_type="application/xml")
|
response_xml, encoding="utf-8", xml_declaration=True
|
||||||
|
).decode()
|
||||||
|
return aiohttp.web.Response(
|
||||||
|
status=207, text=xml_output, content_type="application/xml"
|
||||||
|
)
|
||||||
|
|
||||||
async def handle_propset(self, request):
|
async def handle_propset(self, request):
|
||||||
if not await self.authenticate(request):
|
if not await self.authenticate(request):
|
||||||
@ -801,7 +958,11 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
return aiohttp.web.Response(status=404, text="Resource not found")
|
return aiohttp.web.Response(status=404, text="Resource not found")
|
||||||
lock = await self.is_locked(abs_path, request)
|
lock = await self.is_locked(abs_path, request)
|
||||||
if lock:
|
if lock:
|
||||||
submitted = request.headers.get("Lock-Token", "").strip(" <>").replace("opaquelocktoken:", "")
|
submitted = (
|
||||||
|
request.headers.get("Lock-Token", "")
|
||||||
|
.strip(" <>")
|
||||||
|
.replace("opaquelocktoken:", "")
|
||||||
|
)
|
||||||
if submitted != lock["token"]:
|
if submitted != lock["token"]:
|
||||||
return aiohttp.web.Response(status=423, text="Locked")
|
return aiohttp.web.Response(status=423, text="Locked")
|
||||||
body = await request.read()
|
body = await request.read()
|
||||||
@ -824,17 +985,25 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
response = etree.SubElement(response_xml, "{DAV:}response")
|
response = etree.SubElement(response_xml, "{DAV:}response")
|
||||||
href = etree.SubElement(response, "{DAV:}href")
|
href = etree.SubElement(response, "{DAV:}href")
|
||||||
relative_path = abs_path.relative_to(request["home"]).as_posix()
|
relative_path = abs_path.relative_to(request["home"]).as_posix()
|
||||||
href_path = f"{self.relative_url}/{relative_path}".strip(".").replace("./", "/").replace("//", "/")
|
href_path = (
|
||||||
|
f"{self.relative_url}/{relative_path}".strip(".")
|
||||||
|
.replace("./", "/")
|
||||||
|
.replace("//", "/")
|
||||||
|
)
|
||||||
href.text = href_path
|
href.text = href_path
|
||||||
propstat = etree.SubElement(response, "{DAV:}propstat")
|
propstat = etree.SubElement(response, "{DAV:}propstat")
|
||||||
prop = etree.SubElement(propstat, "{DAV:}prop")
|
prop = etree.SubElement(propstat, "{DAV:}prop")
|
||||||
for child in prop_elem:
|
for child in prop_elem:
|
||||||
ns = child.nsmap.get(None, "DAV:")
|
ns = child.nsmap.get(None, "DAV:")
|
||||||
prop_name = f"{{{ns}}}{child.tag.split('}')[-1]}"
|
prop_name = f"{{{ns}}}{child.tag.split('}')[-1]}"
|
||||||
elem = etree.SubElement(prop, prop_name)
|
etree.SubElement(prop, prop_name)
|
||||||
etree.SubElement(propstat, "{DAV:}status").text = "HTTP/1.1 200 OK"
|
etree.SubElement(propstat, "{DAV:}status").text = "HTTP/1.1 200 OK"
|
||||||
xml_output = etree.tostring(response_xml, encoding="utf-8", xml_declaration=True).decode()
|
xml_output = etree.tostring(
|
||||||
return aiohttp.web.Response(status=207, text=xml_output, content_type="application/xml")
|
response_xml, encoding="utf-8", xml_declaration=True
|
||||||
|
).decode()
|
||||||
|
return aiohttp.web.Response(
|
||||||
|
status=207, text=xml_output, content_type="application/xml"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"PROPSET error: {e}")
|
logging.error(f"PROPSET error: {e}")
|
||||||
return aiohttp.web.Response(status=400, text="Bad Request")
|
return aiohttp.web.Response(status=400, text="Bad Request")
|
||||||
@ -850,7 +1019,11 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
return aiohttp.web.Response(status=404, text="Resource not found")
|
return aiohttp.web.Response(status=404, text="Resource not found")
|
||||||
lock = await self.is_locked(abs_path, request)
|
lock = await self.is_locked(abs_path, request)
|
||||||
if lock:
|
if lock:
|
||||||
submitted = request.headers.get("Lock-Token", "").strip(" <>").replace("opaquelocktoken:", "")
|
submitted = (
|
||||||
|
request.headers.get("Lock-Token", "")
|
||||||
|
.strip(" <>")
|
||||||
|
.replace("opaquelocktoken:", "")
|
||||||
|
)
|
||||||
if submitted != lock["token"]:
|
if submitted != lock["token"]:
|
||||||
return aiohttp.web.Response(status=423, text="Locked")
|
return aiohttp.web.Response(status=423, text="Locked")
|
||||||
body = await request.read()
|
body = await request.read()
|
||||||
@ -877,17 +1050,25 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
response = etree.SubElement(response_xml, "{DAV:}response")
|
response = etree.SubElement(response_xml, "{DAV:}response")
|
||||||
href = etree.SubElement(response, "{DAV:}href")
|
href = etree.SubElement(response, "{DAV:}href")
|
||||||
relative_path = abs_path.relative_to(request["home"]).as_posix()
|
relative_path = abs_path.relative_to(request["home"]).as_posix()
|
||||||
href_path = f"{self.relative_url}/{relative_path}".strip(".").replace("./", "/").replace("//", "/")
|
href_path = (
|
||||||
|
f"{self.relative_url}/{relative_path}".strip(".")
|
||||||
|
.replace("./", "/")
|
||||||
|
.replace("//", "/")
|
||||||
|
)
|
||||||
href.text = href_path
|
href.text = href_path
|
||||||
propstat = etree.SubElement(response, "{DAV:}propstat")
|
propstat = etree.SubElement(response, "{DAV:}propstat")
|
||||||
prop = etree.SubElement(propstat, "{DAV:}prop")
|
prop = etree.SubElement(propstat, "{DAV:}prop")
|
||||||
for child in prop_elem:
|
for child in prop_elem:
|
||||||
ns = child.nsmap.get(None, "DAV:")
|
ns = child.nsmap.get(None, "DAV:")
|
||||||
prop_name = f"{{{ns}}}{child.tag.split('}')[-1]}"
|
prop_name = f"{{{ns}}}{child.tag.split('}')[-1]}"
|
||||||
elem = etree.SubElement(prop, prop_name)
|
etree.SubElement(prop, prop_name)
|
||||||
etree.SubElement(propstat, "{DAV:}status").text = "HTTP/1.1 200 OK"
|
etree.SubElement(propstat, "{DAV:}status").text = "HTTP/1.1 200 OK"
|
||||||
xml_output = etree.tostring(response_xml, encoding="utf-8", xml_declaration=True).decode()
|
xml_output = etree.tostring(
|
||||||
return aiohttp.web.Response(status=207, text=xml_output, content_type="application/xml")
|
response_xml, encoding="utf-8", xml_declaration=True
|
||||||
|
).decode()
|
||||||
|
return aiohttp.web.Response(
|
||||||
|
status=207, text=xml_output, content_type="application/xml"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"PROPDEL error: {e}")
|
logging.error(f"PROPDEL error: {e}")
|
||||||
return aiohttp.web.Response(status=400, text="Bad Request")
|
return aiohttp.web.Response(status=400, text="Bad Request")
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
import pathlib
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
os.chdir("/root")
|
os.chdir("/root")
|
||||||
|
|
||||||
@ -16,7 +14,9 @@ if not pathlib.Path(".welcome.txt").exists():
|
|||||||
os.system("python3 -m venv --prompt '' .venv")
|
os.system("python3 -m venv --prompt '' .venv")
|
||||||
os.system("cp -r /opt/bootstrap/root/.* /root")
|
os.system("cp -r /opt/bootstrap/root/.* /root")
|
||||||
os.system("cp /opt/bootstrap/.welcome.txt /root/.welcome.txt")
|
os.system("cp /opt/bootstrap/.welcome.txt /root/.welcome.txt")
|
||||||
pathlib.Path(".bashrc").write_text(pathlib.Path(".bashrc").read_text() + "\n" + "source .venv/bin/activate")
|
pathlib.Path(".bashrc").write_text(
|
||||||
|
pathlib.Path(".bashrc").read_text() + "\n" + "source .venv/bin/activate"
|
||||||
|
)
|
||||||
os.environ["SNEK"] = "1"
|
os.environ["SNEK"] = "1"
|
||||||
|
|
||||||
if pathlib.Path(".welcome.txt").exists():
|
if pathlib.Path(".welcome.txt").exists():
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user