Added webdav.

This commit is contained in:
retoor 2025-03-29 07:13:23 +01:00
parent fe1b3d6d19
commit 29139d5d0c
54 changed files with 1023 additions and 670 deletions

View File

@ -15,6 +15,8 @@ keywords = ["chat", "snek", "molodetz"]
requires-python = ">=3.12"
dependencies = [
"mkdocs>=1.4.0",
"lxml",
"shed",
"app @ git+https://retoor.molodetz.nl/retoor/app",
"beautifulsoup4",

View File

@ -1,13 +1,14 @@
import pathlib
import asyncio
import logging
import pathlib
import time
from snek.view.threads import ThreadsView
from snek.view.threads import ThreadsView
logging.basicConfig(level=logging.DEBUG)
from concurrent.futures import ThreadPoolExecutor
from aiohttp import web
from aiohttp_session import (
get_session as session_get,
@ -24,23 +25,24 @@ from snek.system import http
from snek.system.cache import Cache
from snek.system.markdown import MarkdownExtension
from snek.system.middleware import cors_middleware
from snek.system.template import LinkifyExtension, PythonExtension,EmojiExtension
from snek.system.profiler import profiler_handler
from snek.system.template import EmojiExtension, LinkifyExtension, PythonExtension
from snek.view.about import AboutHTMLView, AboutMDView
from snek.view.avatar import AvatarView
from snek.view.docs import DocsHTMLView, DocsMDView
from snek.view.drive import DriveView
from snek.view.index import IndexView
from snek.view.login import LoginView
from snek.view.logout import LogoutView
from snek.view.register import RegisterView
from snek.view.rpc import RPCView
from snek.view.search_user import SearchUserView
from snek.view.status import StatusView
from snek.view.web import WebView
from snek.view.terminal import TerminalSocketView, TerminalView
from snek.view.upload import UploadView
from snek.view.search_user import SearchUserView
from snek.view.avatar import AvatarView
from snek.system.profiler import profiler_handler
from snek.view.terminal import TerminalView, TerminalSocketView
from snek.view.drive import DriveView
from concurrent.futures import ThreadPoolExecutor
from snek.view.web import WebView
from snek.webdav import WebdavApplication
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
@ -50,6 +52,15 @@ async def session_middleware(request, handler):
response = await handler(request)
return response
@web.middleware
async def trailing_slash_middleware(request, handler):
if request.path and not request.path.endswith("/"):
# Redirect to the same path with a trailing slash
raise web.HTTPFound(request.path + "/")
return await handler(request)
class Application(BaseApplication):
def __init__(self, *args, **kwargs):
@ -68,9 +79,9 @@ class Application(BaseApplication):
self.jinja2_env.add_extension(LinkifyExtension)
self.jinja2_env.add_extension(PythonExtension)
self.jinja2_env.add_extension(EmojiExtension)
self.setup_router()
self.cache = Cache(self)
self.services = get_services(app=self)
self.mappers = get_mappers(app=self)
@ -81,7 +92,7 @@ class Application(BaseApplication):
async def task_runner(self):
while True:
task = await self.tasks.get()
task = await self.tasks.get()
self.db.begin()
try:
task_start = time.time()
@ -93,20 +104,20 @@ class Application(BaseApplication):
print(ex)
self.db.commit()
async def prepare_database(self,app):
async def prepare_database(self, app):
self.db.query("PRAGMA journal_mode=WAL")
self.db.query("PRAGMA syncnorm=off")
try:
if not self.db["user"].has_index("username"):
self.db["user"].create_index("username", unique=True)
if not self.db["channel_member"].has_index(["channel_uid","user_uid"]):
self.db["channel_member"].create_index(["channel_uid","user_uid"])
if not self.db["channel_message"].has_index(["channel_uid","user_uid"]):
self.db["channel_message"].create_index(["channel_uid","user_uid"])
if not self.db["channel_member"].has_index(["channel_uid", "user_uid"]):
self.db["channel_member"].create_index(["channel_uid", "user_uid"])
if not self.db["channel_message"].has_index(["channel_uid", "user_uid"]):
self.db["channel_message"].create_index(["channel_uid", "user_uid"])
except:
pass
pass
await app.services.drive.prepare_all()
self.loop.create_task(self.task_runner())
@ -145,6 +156,8 @@ class Application(BaseApplication):
self.router.add_view("/terminal.html", TerminalView)
self.router.add_view("/drive.json", DriveView)
self.router.add_view("/drive/{drive}.json", DriveView)
self.webdav = WebdavApplication(self)
self.add_subapp("/webdav", self.webdav)
self.add_subapp(
"/docs",
@ -174,17 +187,21 @@ class Application(BaseApplication):
channels = []
if not context:
context = {}
if request.session.get("uid"):
async for subscribed_channel in self.services.channel_member.find(user_uid=request.session.get("uid"), deleted_at=None, is_banned=False):
if request.session.get("uid"):
async for subscribed_channel in self.services.channel_member.find(
user_uid=request.session.get("uid"), deleted_at=None, is_banned=False
):
item = {}
other_user = await self.services.channel_member.get_other_dm_user(subscribed_channel["channel_uid"], request.session.get("uid"))
other_user = await self.services.channel_member.get_other_dm_user(
subscribed_channel["channel_uid"], request.session.get("uid")
)
parent_object = await subscribed_channel.get_channel()
last_message =await parent_object.get_last_message()
color = None
last_message = await parent_object.get_last_message()
color = None
if last_message:
last_message_user = await last_message.get_user()
color = last_message_user["color"]
item['color'] = color
item["color"] = color
item["last_message_on"] = parent_object["last_message_on"]
item["is_private"] = parent_object["tag"] == "dm"
if other_user:
@ -193,19 +210,22 @@ class Application(BaseApplication):
else:
item["name"] = subscribed_channel["label"]
item["uid"] = subscribed_channel["channel_uid"]
item['new_count'] = subscribed_channel['new_count']
item["new_count"] = subscribed_channel["new_count"]
print(item)
channels.append(item)
channels.sort(key=lambda x: x['last_message_on'] or '', reverse=True)
if not 'channels' in context:
context['channels'] = channels
if not 'user' in context:
context['user'] = await self.services.user.get(uid=request.session.get("uid"))
channels.sort(key=lambda x: x["last_message_on"] or "", reverse=True)
if "channels" not in context:
context["channels"] = channels
if "user" not in context:
context["user"] = await self.services.user.get(
uid=request.session.get("uid")
)
return await super().render_template(template, request, context)
executor = ThreadPoolExecutor(max_workers=200)
loop = asyncio.get_event_loop()
@ -213,8 +233,10 @@ loop.set_default_executor(executor)
app = Application(db_path="sqlite:///snek.db")
async def main():
await web._run_app(app, port=8081, host="0.0.0.0")
if __name__ == "__main__":
asyncio.run(main())

View File

@ -1,31 +1,39 @@
import asyncio
import json
from snek.app import app
async def fix_message(message):
message = dict(
uid=message['uid'],
user_uid=message['user_uid'],
text=message['message'],
sent=message['created_at']
)
user = await app.services.user.get(uid=message['user_uid'])
message['user'] = user and user['username'] or None
return (message['user'] or '') + ': ' + (message['text'] or '')
message = {
"uid": message["uid"],
"user_uid": message["user_uid"],
"text": message["message"],
"sent": message["created_at"],
}
user = await app.services.user.get(uid=message["user_uid"])
message["user"] = user and user["username"] or None
return (message["user"] or "") + ": " + (message["text"] or "")
async def dump_public_channels():
result = []
for channel in app.db['channel'].find(is_private=False,is_listed=True,tag='public'):
for channel in app.db["channel"].find(
is_private=False, is_listed=True, tag="public"
):
print(f"Dumping channel: {channel['label']}.")
result += [await fix_message(record) for record in app.db['channel_message'].find(channel_uid=channel['uid'],order_by='created_at')]
result += [
await fix_message(record)
for record in app.db["channel_message"].find(
channel_uid=channel["uid"], order_by="created_at"
)
]
print("Dump succesfull!")
print("Converting to json.")
print("Converting succesful, now writing to dump.json")
with open("dump.txt","w") as f:
f.write('\n\n'.join(result))
with open("dump.txt", "w") as f:
f.write("\n\n".join(result))
print("Dump written to dump.json")
if __name__ == '__main__':
if __name__ == "__main__":
asyncio.run(dump_public_channels())

View File

@ -16,4 +16,3 @@ class SearchUserForm(Form):
action = FormButtonElement(
name="action", value="submit", text="Search", type="button"
)

View File

@ -3,10 +3,10 @@ import functools
from snek.mapper.channel import ChannelMapper
from snek.mapper.channel_member import ChannelMemberMapper
from snek.mapper.channel_message import ChannelMessageMapper
from snek.mapper.drive import DriveMapper
from snek.mapper.drive_item import DriveItemMapper
from snek.mapper.notification import NotificationMapper
from snek.mapper.user import UserMapper
from snek.mapper.drive import DriveMapper
from snek.mapper.drive_item import DriveItemMapper
from snek.system.object import Object

View File

@ -3,5 +3,5 @@ from snek.system.mapper import BaseMapper
class DriveMapper(BaseMapper):
table_name = 'drive'
model_class = DriveModel
table_name = "drive"
model_class = DriveModel

View File

@ -1,7 +1,8 @@
from snek.model.drive_item import DriveItemModel
from snek.system.mapper import BaseMapper
from snek.model.drive_item import DriveItemModel
class DriveItemMapper(BaseMapper):
model_class = DriveItemModel
table_name = 'drive_item'
table_name = "drive_item"

View File

@ -12,11 +12,16 @@ class ChannelModel(BaseModel):
index = ModelField(name="index", required=True, kind=int, value=1000)
last_message_on = ModelField(name="last_message_on", required=False, kind=str)
async def get_last_message(self)->ChannelMessageModel:
async for model in self.app.services.channel_message.query("SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1",dict(channel_uid=self['uid'])):
return await self.app.services.channel_message.get(uid=model['uid'])
async def get_last_message(self) -> ChannelMessageModel:
async for model in self.app.services.channel_message.query(
"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1",
{"channel_uid": self["uid"]},
):
return await self.app.services.channel_message.get(uid=model["uid"])
return None
async def get_members(self):
return await self.app.services.channel_member.find(channel_uid=self['uid'],deleted_at=None,is_banned=False)
return await self.app.services.channel_member.find(
channel_uid=self["uid"], deleted_at=None, is_banned=False
)

View File

@ -16,25 +16,26 @@ class ChannelMemberModel(BaseModel):
new_count = ModelField(name="new_count", required=False, kind=int, value=0)
async def get_user(self):
return await self.app.services.user.get(uid=self['user_uid'])
return await self.app.services.user.get(uid=self["user_uid"])
async def get_channel(self):
return await self.app.services.channel.get(uid=self['channel_uid'])
return await self.app.services.channel.get(uid=self["channel_uid"])
async def get_name(self):
channel = await self.get_channel()
if channel["tag"] == "dm":
user = await self.get_other_dm_user()
return user['nick']
return channel['name'] or self['label']
return user["nick"]
return channel["name"] or self["label"]
async def get_other_dm_user(self):
channel = await self.get_channel()
if channel["tag"] != "dm":
return None
async for model in self.app.services.channel_member.find(channel_uid=channel['uid']):
if model["uid"] != self['uid']:
async for model in self.app.services.channel_member.find(
channel_uid=channel["uid"]
):
if model["uid"] != self["uid"]:
return await self.app.services.user.get(uid=model["user_uid"])
return await self.get_user()

View File

@ -8,8 +8,8 @@ class ChannelMessageModel(BaseModel):
message = ModelField(name="message", required=True, kind=str)
html = ModelField(name="html", required=False, kind=str)
async def get_user(self)->UserModel:
async def get_user(self) -> UserModel:
return await self.app.services.user.get(uid=self["user_uid"])
async def get_channel(self):
return await self.app.services.channel.get(uid=self["channel_uid"])
return await self.app.services.channel.get(uid=self["channel_uid"])

View File

@ -1,13 +1,14 @@
from snek.system.model import BaseModel,ModelField
from snek.system.model import BaseModel, ModelField
class DriveModel(BaseModel):
user_uid = ModelField(name="user_uid", required=True)
name = ModelField(name='name', required=False, type=str)
name = ModelField(name="name", required=False, type=str)
@property
async def items(self):
async for drive_item in self.app.services.drive_item.find(drive_uid=self['uid']):
async for drive_item in self.app.services.drive_item.find(
drive_uid=self["uid"]
):
yield drive_item

View File

@ -1,18 +1,20 @@
from snek.system.model import BaseModel,ModelField
import mimetypes
from snek.system.model import BaseModel, ModelField
class DriveItemModel(BaseModel):
drive_uid = ModelField(name="drive_uid", required=True,kind=str)
name = ModelField(name="name", required=True,kind=str)
path = ModelField(name="path", required=True,kind=str)
file_type = ModelField(name="file_type", required=True,kind=str)
file_size = ModelField(name="file_size", required=True,kind=int)
drive_uid = ModelField(name="drive_uid", required=True, kind=str)
name = ModelField(name="name", required=True, kind=str)
path = ModelField(name="path", required=True, kind=str)
file_type = ModelField(name="file_type", required=True, kind=str)
file_size = ModelField(name="file_size", required=True, kind=int)
@property
def extension(self):
return self['name'].split('.')[-1]
@property
return self["name"].split(".")[-1]
@property
def mime_type(self):
mimetype,_ = mimetypes.guess_type(self['name'])
return mimetype
mimetype, _ = mimetypes.guess_type(self["name"])
return mimetype

View File

@ -18,10 +18,7 @@ class UserModel(BaseModel):
regex=r"^[a-zA-Z0-9_-+/]+$",
)
color = ModelField(
name ="color",
required=True,
regex=r"^#[0-9a-fA-F]{6}$",
kind=str
name="color", required=True, regex=r"^#[0-9a-fA-F]{6}$", kind=str
)
email = ModelField(
name="email",
@ -33,5 +30,7 @@ class UserModel(BaseModel):
last_ping = ModelField(name="last_ping", required=False, kind=str)
async def get_channel_members(self):
async for channel_member in self.app.services.channel_member.find(user_uid=self['uid'],is_banned=False,deleted_at=None):
async for channel_member in self.app.services.channel_member.find(
user_uid=self["uid"], is_banned=False, deleted_at=None
):
yield channel_member

View File

@ -4,12 +4,12 @@ from snek.service.channel import ChannelService
from snek.service.channel_member import ChannelMemberService
from snek.service.channel_message import ChannelMessageService
from snek.service.chat import ChatService
from snek.service.drive import DriveService
from snek.service.drive_item import DriveItemService
from snek.service.notification import NotificationService
from snek.service.socket import SocketService
from snek.service.user import UserService
from snek.service.util import UtilService
from snek.service.drive import DriveService
from snek.service.drive_item import DriveItemService
from snek.system.object import Object
@ -26,7 +26,7 @@ def get_services(app):
"notification": NotificationService(app=app),
"util": UtilService(app=app),
"drive": DriveService(app=app),
"drive_item": DriveItemService(app=app)
"drive_item": DriveItemService(app=app),
}
)

View File

@ -1,30 +1,28 @@
from snek.system.service import BaseService
from datetime import datetime
from datetime import datetime
from snek.system.model import now
from snek.system.service import BaseService
from snek.system.model import now
class ChannelService(BaseService):
mapper_name = "channel"
async def get(
self,
uid=None,
**kwargs):
async def get(self, uid=None, **kwargs):
if uid:
kwargs['uid'] = uid
result = await super().get(**kwargs)
if result:
return result
del kwargs['uid']
kwargs['name'] = uid
kwargs["uid"] = uid
result = await super().get(**kwargs)
if result:
return result
kwargs['name'] = '#' + uid
del kwargs["uid"]
kwargs["name"] = uid
result = await super().get(**kwargs)
if result:
return result
return None
kwargs["name"] = "#" + uid
result = await super().get(**kwargs)
if result:
return result
return None
return await super().get(**kwargs)
async def create(
@ -53,38 +51,34 @@ class ChannelService(BaseService):
raise Exception(f"Failed to create channel: {model.errors}.")
async def get_dm(self, user1, user2):
channel_member = await self.services.channel_member.get_dm(
user1, user2
)
channel_member = await self.services.channel_member.get_dm(user1, user2)
if channel_member:
return await self.get(uid=channel_member["channel_uid"])
channel = await self.create(
"DM", user1, tag="dm"
)
await self.services.channel_member.create_dm(
channel["uid"], user1, user2
)
return channel
channel = await self.create("DM", user1, tag="dm")
await self.services.channel_member.create_dm(channel["uid"], user1, user2)
return channel
async def get_users(self, channel_uid):
users = []
async for channel_member in self.services.channel_member.find(
channel_uid=channel_uid,
is_banned=False,
is_muted=False,
deleted_at=None,
):
user = await self.services.user.get(uid=channel_member["user_uid"])
user = await self.services.user.get(uid=channel_member["user_uid"])
if user:
yield user
async def get_online_users(self, channel_uid):
users = []
async for user in self.get_users(channel_uid):
if not user["last_ping"]:
continue
if (datetime.fromisoformat(now()) - datetime.fromisoformat(user["last_ping"])).total_seconds() < 20:
yield user
if (
datetime.fromisoformat(now())
- datetime.fromisoformat(user["last_ping"])
).total_seconds() < 20:
yield user
async def get_for_user(self, user_uid):
async for channel_member in self.services.channel_member.find(
@ -93,7 +87,7 @@ class ChannelService(BaseService):
deleted_at=None,
):
channel = await self.get(uid=channel_member["channel_uid"])
yield channel
yield channel
async def ensure_public_channel(self, created_by_uid):
model = await self.get(is_listed=True, tag="public")

View File

@ -6,10 +6,7 @@ class ChannelMemberService(BaseService):
mapper_name = "channel_member"
async def mark_as_read(self, channel_uid, user_uid):
channel_member = await self.get(
channel_uid=channel_uid,
user_uid=user_uid
)
channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)
channel_member["new_count"] = 0
return await self.save(channel_member)
@ -24,7 +21,7 @@ class ChannelMemberService(BaseService):
):
model = await self.get(channel_uid=channel_uid, user_uid=user_uid)
if model:
if model['is_banned']:
if model["is_banned"]:
return False
return model
model = await self.new()
@ -39,30 +36,32 @@ class ChannelMemberService(BaseService):
if await self.save(model):
return model
raise Exception(f"Failed to create channel member: {model.errors}.")
async def get_dm(self,from_user, to_user):
async for model in self.query("SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') INNER JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = channel.uid AND channel_member2.user_uid = :to_user) WHERE channel_member.user_uid=:from_user " ,dict(from_user=from_user, to_user=to_user)):
async def get_dm(self, from_user, to_user):
async for model in self.query(
"SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') INNER JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = channel.uid AND channel_member2.user_uid = :to_user) WHERE channel_member.user_uid=:from_user ",
{"from_user": from_user, "to_user": to_user},
):
return model
if not from_user == to_user:
return None
async for model in self.query("SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') LEFT JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = NULL AND channel_member2.user_uid = NULL) WHERE channel_member.user_uid=:from_user " ,dict(from_user=from_user, to_user=to_user)):
return model
return None
async for model in self.query(
"SELECT channel_member.* FROM channel_member INNER JOIN channel ON (channel.uid = channel_member.channel_uid and channel.tag = 'dm') LEFT JOIN channel_member AS channel_member2 ON(channel_member2.channel_uid = NULL AND channel_member2.user_uid = NULL) WHERE channel_member.user_uid=:from_user ",
{"from_user": from_user, "to_user": to_user},
):
return model
async def get_other_dm_user(self, channel_uid, user_uid):
channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)
channel = await self.services.channel.get(uid=channel_member['channel_uid'])
channel = await self.services.channel.get(uid=channel_member["channel_uid"])
if channel["tag"] != "dm":
return None
async for model in self.services.channel_member.find(channel_uid=channel_uid):
if model["uid"] != channel_member['uid']:
if model["uid"] != channel_member["uid"]:
return await self.services.user.get(uid=model["user_uid"])
async def create_dm(self,channel_uid, from_user_uid, to_user_uid):
async def create_dm(self, channel_uid, from_user_uid, to_user_uid):
result = await self.create(channel_uid, from_user_uid)
await self.create(channel_uid, to_user_uid)
return result
return result

View File

@ -1,71 +1,93 @@
from snek.system.service import BaseService
import jinja2
class ChannelMessageService(BaseService):
mapper_name = "channel_message"
async def create(self, channel_uid, user_uid, message):
model = await self.new()
model["channel_uid"] = channel_uid
model["user_uid"] = user_uid
model["message"] = message
context = {
}
record = model.record
context = {}
record = model.record
context.update(record)
user = await self.app.services.user.get(uid=user_uid)
context.update(dict(
user_uid=user['uid'],
username=user['username'],
user_nick=user['nick'],
color=user['color']
))
context.update(
{
"user_uid": user["uid"],
"username": user["username"],
"user_nick": user["nick"],
"color": user["color"],
}
)
try:
template = self.app.jinja2_env.get_template("message.html")
model["html"] = template.render(**context)
except Exception as ex:
print(ex,flush=True)
print(ex, flush=True)
if await self.save(model):
return model
raise Exception(f"Failed to create channel message: {model.errors}.")
async def to_extended_dict(self, message):
user = await self.services.user.get(uid=message["user_uid"])
if not user:
return {}
return {
"uid": message["uid"],
"color": user['color'],
"color": user["color"],
"user_uid": message["user_uid"],
"channel_uid": message["channel_uid"],
"user_nick": user['nick'],
"user_nick": user["nick"],
"message": message["message"],
"created_at": message["created_at"],
"html": message['html'],
"username": user['username']
"html": message["html"],
"username": user["username"],
}
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):
results = []
offset = page * page_size
offset = page * page_size
try:
if timestamp:
async for model in self.query("SELECT * FROM channel_message WHERE channel_uid=:channel_uid AND created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",dict(channel_uid=channel_uid, page_size=page_size, offset=offset, timestamp=timestamp)):
async for model in self.query(
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid AND created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",
{
"channel_uid": channel_uid,
"page_size": page_size,
"offset": offset,
"timestamp": timestamp,
},
):
results.append(model)
elif page > 0:
async for model in self.query("SELECT * FROM channel_message WHERE channel_uid=:channel_uid WHERE created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size",dict(channel_uid=channel_uid, page_size=page_size, offset=offset,timestamp=timestamp )):
async for model in self.query(
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid WHERE created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size",
{
"channel_uid": channel_uid,
"page_size": page_size,
"offset": offset,
"timestamp": timestamp,
},
):
results.append(model)
else:
async for model in self.query("SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",dict(channel_uid=channel_uid, page_size=page_size, offset=offset)):
else:
async for model in self.query(
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",
{
"channel_uid": channel_uid,
"page_size": page_size,
"offset": offset,
},
):
results.append(model)
except:
except:
pass
results.sort(key=lambda x: x['created_at'])
return results
results.sort(key=lambda x: x["created_at"])
return results

View File

@ -1,40 +1,39 @@
from snek.system.model import now
from snek.system.service import BaseService
class ChatService(BaseService):
async def send(self,user_uid, channel_uid, message):
async def send(self, user_uid, channel_uid, message):
channel = await self.services.channel.get(uid=channel_uid)
if not channel:
raise Exception("Channel not found.")
channel_message = await self.services.channel_message.create(
channel_uid,
user_uid,
message
channel_uid, user_uid, message
)
channel_message_uid = channel_message["uid"]
user = await self.services.user.get(uid=user_uid)
channel['last_message_on'] = now()
channel["last_message_on"] = now()
await self.services.channel.save(channel)
await self.services.socket.broadcast(channel_uid, dict(
message=channel_message["message"],
html=channel_message["html"],
user_uid=user_uid,
color=user['color'],
channel_uid=channel_uid,
created_at=channel_message["created_at"],
updated_at=None,
username=user['username'],
uid=channel_message['uid'],
user_nick=user['nick']
))
await self.app.create_task(self.services.notification.create_channel_message(channel_message_uid))
await self.services.socket.broadcast(
channel_uid,
{
"message": channel_message["message"],
"html": channel_message["html"],
"user_uid": user_uid,
"color": user["color"],
"channel_uid": channel_uid,
"created_at": channel_message["created_at"],
"updated_at": None,
"username": user["username"],
"uid": channel_message["uid"],
"user_nick": user["nick"],
},
)
await self.app.create_task(
self.services.notification.create_channel_message(channel_message_uid)
)
return True

View File

@ -2,14 +2,90 @@ from snek.system.service import BaseService
class DriveService(BaseService):
mapper_name = "drive"
EXTENSIONS_PICTURES = ["jpg","jpeg","png","gif","svg","webp","tiff"]
EXTENSIONS_VIDEOS = ["mp4","m4v","mov","wmv","webm","mkv","mpg","mpeg","avi","ogv","ogg","flv","3gp","3g2"]
EXTENSIONS_ARCHIVES = ["zip","rar","7z","tar","tar.gz","tar.xz","tar.bz2","tar.lzma","tar.lz"]
EXTENSIONS_AUDIO = ["mp3","wav","ogg","flac","m4a","wma","aac","opus","aiff","au","mid","midi"]
EXTENSIONS_DOCS = ["pdf","doc","docx","xls","xlsx","ppt","pptx","txt","md","json","csv","xml","html","css","js","py","sql","rs","toml","yml","yaml","ini","conf","config","log","csv","tsv","java","cs","csproj","scss","less","sass","json","lock","lock.json","jsonl"]
EXTENSIONS_PICTURES = ["jpg", "jpeg", "png", "gif", "svg", "webp", "tiff"]
EXTENSIONS_VIDEOS = [
"mp4",
"m4v",
"mov",
"wmv",
"webm",
"mkv",
"mpg",
"mpeg",
"avi",
"ogv",
"ogg",
"flv",
"3gp",
"3g2",
]
EXTENSIONS_ARCHIVES = [
"zip",
"rar",
"7z",
"tar",
"tar.gz",
"tar.xz",
"tar.bz2",
"tar.lzma",
"tar.lz",
]
EXTENSIONS_AUDIO = [
"mp3",
"wav",
"ogg",
"flac",
"m4a",
"wma",
"aac",
"opus",
"aiff",
"au",
"mid",
"midi",
]
EXTENSIONS_DOCS = [
"pdf",
"doc",
"docx",
"xls",
"xlsx",
"ppt",
"pptx",
"txt",
"md",
"json",
"csv",
"xml",
"html",
"css",
"js",
"py",
"sql",
"rs",
"toml",
"yml",
"yaml",
"ini",
"conf",
"config",
"log",
"csv",
"tsv",
"java",
"cs",
"csproj",
"scss",
"less",
"sass",
"json",
"lock",
"lock.json",
"jsonl",
]
async def get_drive_name_by_extension(self, extension):
if extension.startswith("."):
@ -26,54 +102,52 @@ class DriveService(BaseService):
return "Documents"
return "My Drive"
async def get_drive_by_extension(self,user_uid, extension):
async def get_drive_by_extension(self, user_uid, extension):
name = await self.get_drive_name_by_extension(extension)
return await self.get_or_create(user_uid=user_uid,name=name)
return await self.get_or_create(user_uid=user_uid, name=name)
async def get_by_user(self, user_uid,name=None):
kwargs = dict(
user_uid = user_uid
)
async def get_by_user(self, user_uid, name=None):
kwargs = {"user_uid": user_uid}
async for model in self.find(**kwargs):
if not name:
yield model
elif model['name'] == name:
yield model
elif not model['name'] and name == 'My Drive':
model['name'] = 'My Drive'
elif model["name"] == name:
yield model
elif not model["name"] and name == "My Drive":
model["name"] = "My Drive"
await self.save(model)
yield model
yield model
async def get_or_create(self, user_uid,name=None,extensions=None):
kwargs = dict(user_uid=user_uid)
async def get_or_create(self, user_uid, name=None, extensions=None):
kwargs = {"user_uid": user_uid}
if name:
kwargs['name'] = name
kwargs["name"] = name
async for model in self.get_by_user(**kwargs):
return model
return model
model = await self.new()
model['user_uid'] = user_uid
model['name'] = name
model["user_uid"] = user_uid
model["name"] = name
await self.save(model)
return model
return model
async def prepare_default_drives(self):
async for drive_item in self.services.drive_item.find():
extension = drive_item.extension
drive = await self.get_drive_by_extension(drive_item['user_uid'],extension)
if not drive_item['drive_uid'] == drive['uid']:
drive_item['drive_uid'] = drive['uid']
drive = await self.get_drive_by_extension(drive_item["user_uid"], extension)
if not drive_item["drive_uid"] == drive["uid"]:
drive_item["drive_uid"] = drive["uid"]
await self.services.drive_item.save(drive_item)
async def prepare_default_drives_for_user(self, user_uid):
await self.get_or_create(user_uid=user_uid,name="My Drive")
await self.get_or_create(user_uid=user_uid,name="Shared Drive")
await self.get_or_create(user_uid=user_uid,name="Pictures")
await self.get_or_create(user_uid=user_uid,name="Videos")
await self.get_or_create(user_uid=user_uid,name="Archives")
await self.get_or_create(user_uid=user_uid,name="Documents")
await self.get_or_create(user_uid=user_uid, name="My Drive")
await self.get_or_create(user_uid=user_uid, name="Shared Drive")
await self.get_or_create(user_uid=user_uid, name="Pictures")
await self.get_or_create(user_uid=user_uid, name="Videos")
await self.get_or_create(user_uid=user_uid, name="Archives")
await self.get_or_create(user_uid=user_uid, name="Documents")
async def prepare_all(self):
await self.prepare_default_drives()
async for user in self.services.user.find():
await self.prepare_default_drives_for_user(user['uid'])
await self.prepare_default_drives_for_user(user["uid"])

View File

@ -1,19 +1,19 @@
from snek.system.service import BaseService
from snek.system.service import BaseService
class DriveItemService(BaseService):
mapper_name = "drive_item"
async def create(self, drive_uid, name, path, type_,size):
model = await self.new()
model['drive_uid'] = drive_uid
model['name'] = name
model['path'] = str(path)
model['extension'] = str(name).split(".")[-1]
model['file_type'] = type_
model['file_size'] = size
async def create(self, drive_uid, name, path, type_, size):
model = await self.new()
model["drive_uid"] = drive_uid
model["name"] = name
model["path"] = str(path)
model["extension"] = str(name).split(".")[-1]
model["file_type"] = type_
model["file_size"] = size
if await self.save(model):
return model
return model
errors = await model.errors
raise Exception(f"Failed to create drive item: {errors}.")

View File

@ -1,5 +1,6 @@
from snek.system.service import BaseService
from snek.system.model import now
from snek.system.service import BaseService
class NotificationService(BaseService):
mapper_name = "notification"
@ -7,13 +8,16 @@ class NotificationService(BaseService):
async def mark_as_read(self, user_uid, channel_message_uid):
model = await self.get(user_uid, object_uid=channel_message_uid)
if not model:
return False
model['read_at'] = now()
return False
model["read_at"] = now()
await self.save(model)
return True
async def get_unread_stats(self,user_uid):
records = await self.query("SELECT object_type, COUNT(*) as count FROM notification WHERE user_uid=:user_uid AND read_at IS NULL GROUP BY object_type",dict(user_uid=user_uid))
return True
async def get_unread_stats(self, user_uid):
await self.query(
"SELECT object_type, COUNT(*) as count FROM notification WHERE user_uid=:user_uid AND read_at IS NULL GROUP BY object_type",
{"user_uid": user_uid},
)
async def create(self, object_uid, object_type, user_uid, message):
model = await self.new()
@ -37,10 +41,10 @@ class NotificationService(BaseService):
is_muted=False,
deleted_at=None,
):
if not channel_member['new_count']:
channel_member['new_count'] = 0
channel_member['new_count'] += 1
if not channel_member["new_count"]:
channel_member["new_count"] = 0
channel_member["new_count"] += 1
usr = await self.services.user.get(uid=channel_member["user_uid"])
if not usr:
continue
@ -55,7 +59,7 @@ class NotificationService(BaseService):
)
try:
await self.save(model)
except Exception as ex:
except Exception:
raise Exception(f"Failed to create notification: {model.errors}.")
self.app.db.commit()

View File

@ -1,6 +1,4 @@
from snek.model.user import UserModel
from snek.system.service import BaseService
@ -9,28 +7,27 @@ class SocketService(BaseService):
class Socket:
def __init__(self, ws, user: UserModel):
self.ws = ws
self.is_connected = True
self.user = user
self.is_connected = True
self.user = user
async def send_json(self, data):
if not self.is_connected:
return False
return False
try:
await self.ws.send_json(data)
except Exception as ex:
print(ex,flush=True)
print(ex, flush=True)
self.is_connected = False
return True
return True
async def close(self):
if not self.is_connected:
return True
return True
await self.ws.close()
self.is_connected = False
return True
return True
def __init__(self, app):
super().__init__(app)
@ -42,32 +39,31 @@ class SocketService(BaseService):
s = self.Socket(ws, await self.app.services.user.get(uid=user_uid))
self.sockets.add(s)
if not self.users.get(user_uid):
self.users[user_uid] = set()
self.users[user_uid] = set()
self.users[user_uid].add(s)
async def subscribe(self, ws,channel_uid, user_uid):
async def subscribe(self, ws, channel_uid, user_uid):
return
if not channel_uid in self.subscriptions:
if channel_uid not in self.subscriptions:
self.subscriptions[channel_uid] = set()
s = self.Socket(ws,await self.app.services.user.get(uid=user_uid))
s = self.Socket(ws, await self.app.services.user.get(uid=user_uid))
self.subscriptions[channel_uid].add(s)
async def send_to_user(self, user_uid, message):
count = 0
for s in self.users.get(user_uid,[]):
count = 0
for s in self.users.get(user_uid, []):
if await s.send_json(message):
count += 1
return count
count += 1
return count
async def broadcast(self, channel_uid, message):
count = 0
async for channel_member in self.app.services.channel_member.find(channel_uid=channel_uid):
await self.send_to_user(channel_member["user_uid"],message)
return True
async for channel_member in self.app.services.channel_member.find(
channel_uid=channel_uid
):
await self.send_to_user(channel_member["user_uid"], message)
return True
async def delete(self, ws):
for s in [sock for sock in self.sockets if sock.ws == ws]:
await s.close()
self.sockets.remove(s)

View File

@ -1,16 +1,18 @@
import pathlib
from snek.system import security
from snek.system.service import BaseService
class UserService(BaseService):
mapper_name = "user"
async def search(self, query, **kwargs):
query = query.strip().lower()
if not query:
raise []
results = []
async for result in self.find(username=dict(ilike='%' + query + '%'), **kwargs):
async for result in self.find(username={"ilike": "%" + query + "%"}, **kwargs):
results.append(result)
return results
@ -23,17 +25,32 @@ class UserService(BaseService):
return True
async def save(self, user):
if not user['color']:
user['color'] = await self.services.util.random_light_hex_color()
if not user["color"]:
user["color"] = await self.services.util.random_light_hex_color()
return await super().save(user)
async def authenticate(self, username, password):
print(username, password, flush=True)
success = await self.validate_login(username, password)
print(success, flush=True)
if not success:
return None
model = await self.get(username=username, deleted_at=None)
return model
async def get_home_folder(self, user_uid):
folder = pathlib.Path(f"./drive/{user_uid}")
if not folder.exists():
folder.mkdir(parents=True, exist_ok=True)
return folder
async def register(self, email, username, password):
if await self.exists(username=username):
raise Exception("User already exists.")
model = await self.new()
model["nick"] = username
model['color'] = await self.services.util.random_light_hex_color()
model["color"] = await self.services.util.random_light_hex_color()
model.email.value = email
model.username.value = username
model.password.value = await security.hash(password)

View File

@ -1,15 +1,14 @@
import random
from snek.system.service import BaseService
class UtilService(BaseService):
async def random_light_hex_color(self):
r = random.randint(128, 255)
g = random.randint(128, 255)
b = random.randint(128, 255)
return "#{:02x}{:02x}{:02x}".format(r, g, b)
return f"#{r:02x}{g:02x}{b:02x}"

View File

@ -20,13 +20,13 @@ class Cache:
try:
self.lru.pop(self.lru.index(args))
except:
#print("Cache miss!", args, flush=True)
# print("Cache miss!", args, flush=True)
return None
self.lru.insert(0, args)
while len(self.lru) > self.max_items:
self.cache.pop(self.lru[-1])
self.lru.pop()
#print("Cache hit!", args, flush=True)
# print("Cache hit!", args, flush=True)
return self.cache[args]
def json_default(self, value):
@ -61,7 +61,7 @@ class Cache:
if is_new:
self.version += 1
#print(f"Cache store! {len(self.lru)} items. New version:", self.version, flush=True)
# print(f"Cache store! {len(self.lru)} items. New version:", self.version, flush=True)
async def delete(self, args):
if args in self.cache:

View File

@ -1,7 +1,7 @@
# Original source: https://brandonjay.dev/posts/2021/render-markdown-html-in-python-with-jinja2
from types import SimpleNamespace
from html import escape
from app.cache import time_cache_async
from mistune import HTMLRenderer, Markdown
from pygments import highlight
@ -24,21 +24,21 @@ class MarkdownRenderer(HTMLRenderer):
def _escape(self, str):
return str ##escape(str)
def get_lexer(self, lang, default='bash'):
def get_lexer(self, lang, default="bash"):
try:
return get_lexer_by_name(lang, stripall=True)
except:
return get_lexer_by_name(default, stripall=True)
def block_code(self, code, lang=None, info=None):
if not lang:
lang = info
if not lang:
lang = 'bash'
lang = "bash"
lexer = self.get_lexer(lang)
formatter = html.HtmlFormatter(lineseparator="<br>")
result = highlight(code, lexer, formatter)
return result
return result
def render(self):
markdown_string = self.app.template_path.joinpath(self.template).read_text()

View File

@ -20,7 +20,9 @@ async def no_cors_middleware(request, handler):
async def cors_allow_middleware(request, handler):
response = await handler(request)
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE"
response.headers["Access-Control-Allow-Methods"] = (
"GET, POST, OPTIONS, PUT, DELETE, MOVE, COPY, HEAD, LOCK, UNLOCK, PATCH, PROPFIND"
)
response.headers["Access-Control-Allow-Headers"] = "*"
response.headers["Access-Control-Allow-Credentials"] = "true"
return response
@ -28,17 +30,12 @@ async def cors_allow_middleware(request, handler):
@web.middleware
async def cors_middleware(request, handler):
if request.method == "OPTIONS":
response = web.Response()
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = (
"GET, POST, PUT, DELETE, OPTIONS"
)
response.headers["Access-Control-Allow-Headers"] = "*"
response.headers["Access-Control-Allow-Credentials"] = "true"
return response
if request.headers.get("Allow"):
return await handler(request)
response = await handler(request)
if request.headers.get("Allow"):
return response
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "*"

View File

@ -1,24 +1,27 @@
import cProfile
import pstats
import sys
import sys
from aiohttp import web
profiler = None
import io
import io
@web.middleware
async def profile_middleware(request, handler):
global profiler
global profiler
if not profiler:
profiler = cProfile.Profile()
profiler.enable()
response = await handler(request)
profiler.disable()
stats = pstats.Stats(profiler, stream=sys.stdout)
stats.sort_stats('cumulative')
stats.print_stats()
stats.sort_stats("cumulative")
stats.print_stats()
return response
async def profiler_handler(request):
output = io.StringIO()
stats = pstats.Stats(profiler, stream=output)
@ -27,17 +30,17 @@ async def profiler_handler(request):
stats.print_stats()
return web.Response(text=output.getvalue())
class Profiler:
def __init__(self):
global profiler
global profiler
if profiler is None:
profiler = cProfile.Profile()
self.profiler = profiler
async def __aenter__(self):
self.profiler.enable()
self.profiler.enable()
async def __aexit__(self, *args, **kwargs):
self.profiler.disable()

View File

@ -58,7 +58,7 @@ class BaseService:
raise Exception(f"Couldn't save model. Errors: f{errors}")
async def find(self, **kwargs):
if not "_limit" in kwargs or int(kwargs.get("_limit")) > 30:
if "_limit" not in kwargs or int(kwargs.get("_limit")) > 30:
kwargs["_limit"] = 60
async for model in self.mapper.find(**kwargs):
yield model

View File

@ -1,15 +1,21 @@
import re
from types import SimpleNamespace
from bs4 import BeautifulSoup
import re
import emoji
import emoji
from bs4 import BeautifulSoup
from jinja2 import TemplateSyntaxError, nodes
from jinja2.ext import Extension
from jinja2.nodes import Const
emoji.EMOJI_DATA['<img src="/emoji/snek1.gif" />'] = {"en": ":snek1:","status":2,"E":0.6, "alias":[":snek1:"]}
emoji.EMOJI_DATA['<img src="/emoji/snek1.gif" />'] = {
"en": ":snek1:",
"status": 2,
"E": 0.6,
"alias": [":snek1:"],
}
emoji.EMOJI_DATA["""
emoji.EMOJI_DATA[
"""
@ -64,62 +70,91 @@ emoji.EMOJI_DATA["""
"""] = {"en": ":a1:","status":2,"E":0.6, "alias":[":a1:"]}
"""
] = {"en": ":a1:", "status": 2, "E": 0.6, "alias": [":a1:"]}
def set_link_target_blank(text):
soup = BeautifulSoup(text, 'html.parser')
soup = BeautifulSoup(text, "html.parser")
for element in soup.find_all("a"):
element.attrs['target'] = '_blank'
element.attrs['rel'] = 'noopener noreferrer'
element.attrs['referrerpolicy'] = 'no-referrer'
element.attrs['href'] = element.attrs['href'].strip(".").strip(",")
for element in soup.find_all("a"):
element.attrs["target"] = "_blank"
element.attrs["rel"] = "noopener noreferrer"
element.attrs["referrerpolicy"] = "no-referrer"
element.attrs["href"] = element.attrs["href"].strip(".").strip(",")
return str(soup)
def embed_youtube(text):
soup = BeautifulSoup(text, 'html.parser')
for element in soup.find_all("a"):
if element.attrs['href'].startswith("https://www.you") and "?v=" in element.attrs["href"]:
soup = BeautifulSoup(text, "html.parser")
for element in soup.find_all("a"):
if (
element.attrs["href"].startswith("https://www.you")
and "?v=" in element.attrs["href"]
):
video_name = element.attrs["href"].split("?v=")[1].split("&")[0]
embed_template = f'<iframe width="560" height="315" src="https://www.youtube.com/embed/{video_name}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>'
element.replace_with(BeautifulSoup(embed_template, 'html.parser'))
element.replace_with(BeautifulSoup(embed_template, "html.parser"))
return str(soup)
def embed_image(text):
soup = BeautifulSoup(text, 'html.parser')
for element in soup.find_all("a"):
for extension in [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".tiff", ".ico", ".heif"]:
if extension in element.attrs['href'].lower():
soup = BeautifulSoup(text, "html.parser")
for element in soup.find_all("a"):
for extension in [
".png",
".jpg",
".jpeg",
".gif",
".webp",
".svg",
".bmp",
".tiff",
".ico",
".heif",
]:
if extension in element.attrs["href"].lower():
embed_template = f'<img src="{element.attrs["href"]}" title="{element.attrs["href"]}" alt="{element.attrs["href"]}" />'
element.replace_with(BeautifulSoup(embed_template, 'html.parser'))
element.replace_with(BeautifulSoup(embed_template, "html.parser"))
return str(soup)
def embed_media(text):
soup = BeautifulSoup(text, 'html.parser')
for element in soup.find_all("a"):
for extension in [".mp4", ".mp3", ".wav", ".ogg", ".webm", ".flac", ".aac",".mpg",".avi",".wmv"]:
if extension in element.attrs['href'].lower():
soup = BeautifulSoup(text, "html.parser")
for element in soup.find_all("a"):
for extension in [
".mp4",
".mp3",
".wav",
".ogg",
".webm",
".flac",
".aac",
".mpg",
".avi",
".wmv",
]:
if extension in element.attrs["href"].lower():
embed_template = f'<video controls> <source src="{element.attrs["href"]}">Your browser does not support the video tag.</video>'
element.replace_with(BeautifulSoup(embed_template, 'html.parser'))
element.replace_with(BeautifulSoup(embed_template, "html.parser"))
return str(soup)
def linkify_https(text):
if not "https://" in text:
return text
if "https://" not in text:
return text
url_pattern = r'(?<!["\'])\bhttps://[^\s<>()]+(?<!\.)'
soup = BeautifulSoup(text, 'html.parser')
soup = BeautifulSoup(text, "html.parser")
for element in soup.find_all(text=True):
for element in soup.find_all(text=True):
parent = element.parent
if parent.name in ['a', 'script', 'style']:
if parent.name in ["a", "script", "style"]:
continue
new_text = re.sub(url_pattern, r'<a href="\g<0>">\g<0></a>', element)
element.replace_with(BeautifulSoup(new_text, 'html.parser'))
element.replace_with(BeautifulSoup(new_text, "html.parser"))
return set_link_target_blank(str(soup))
@ -140,8 +175,7 @@ class EmojiExtension(Extension):
).set_lineno(line_number)
def _to_html(self, md_file, caller):
return emoji.emojize(caller(),language='alias')
return emoji.emojize(caller(), language="alias")
class LinkifyExtension(Extension):
@ -170,6 +204,7 @@ class LinkifyExtension(Extension):
result = embed_youtube(result)
return result
class PythonExtension(Extension):
tags = {"py3"}
@ -186,26 +221,26 @@ class PythonExtension(Extension):
).set_lineno(line_number)
def _to_html(self, md_file, caller):
def fn(source):
import subprocess
import subprocess
import pathlib
from pathlib import Path
import os
import sys
import requests
import subprocess
def system(command):
if isinstance(command):
command = command.split(" ")
from io import StringIO
from io import StringIO
stdout = StringIO()
subprocess.run(command,stderr=stdout,stdout=stdout,text=True)
subprocess.run(command, stderr=stdout, stdout=stdout, text=True)
return stdout.getvalue()
to_write = []
def render(text):
global to_write
global to_write
to_write.append(text)
exec(source)
return "".join(to_write)
return str(fn(caller()))

View File

@ -1,30 +1,27 @@
import asyncio
import aiohttp
import aiohttp.web
import os
import pty
import shlex
import subprocess
import pathlib
commands = {
'alpine': 'docker run -it alpine /bin/sh',
'r': 'docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh',
"alpine": "docker run -it alpine /bin/sh",
"r": "docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh",
}
class TerminalSession:
def __init__(self,command):
def __init__(self, command):
self.master, self.slave = pty.openpty()
self.sockets =[]
self.history = b''
self.history_size = 1024*20
self.sockets = []
self.history = b""
self.history_size = 1024 * 20
self.process = subprocess.Popen(
command.split(" "),
stdin=self.slave,
stdout=self.slave,
stderr=self.slave,
bufsize=0,
universal_newlines=True
universal_newlines=True,
)
async def add_websocket(self, ws):
@ -35,11 +32,11 @@ class TerminalSession:
if len(self.sockets) > 1 and self.history:
start = 0
try:
start = self.history.index(b'\n')
start = self.history.index(b"\n")
except ValueError:
pass
pass
await ws.send_bytes(self.history[start:])
return
return
loop = asyncio.get_event_loop()
while True:
try:
@ -48,9 +45,10 @@ class TerminalSession:
break
self.history += data
if len(self.history) > self.history_size:
self.history = self.history[:0-self.history_size]
self.history = self.history[: 0 - self.history_size]
try:
for ws in self.sockets: await ws.send_bytes(data) # Send raw bytes for ANSI support
for ws in self.sockets:
await ws.send_bytes(data) # Send raw bytes for ANSI support
except:
self.sockets.remove(ws)
except Exception:

View File

@ -12,9 +12,9 @@ class BaseView(web.View):
return web.HTTPFound("/")
return await super()._iter()
@property
@property
def base_url(self):
return str(self.request.url.with_path('').with_query(''))
return str(self.request.url.with_path("").with_query(""))
@property
def app(self):

View File

@ -26,12 +26,14 @@
from snek.system.view import BaseView
class AboutHTMLView(BaseView):
async def get(self):
return await self.render_template("about.html")
class AboutMDView(BaseView):
async def get(self):
return await self.render_template("about.md")
return await self.render_template("about.md")

View File

@ -23,11 +23,14 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from multiavatar import multiavatar
import uuid
from aiohttp import web
from multiavatar import multiavatar
from snek.system.view import BaseView
class AvatarView(BaseView):
login_required = False
@ -36,6 +39,6 @@ class AvatarView(BaseView):
if uid == "unique":
uid = str(uuid.uuid4())
avatar = multiavatar.multiavatar(uid, True, None)
response = web.Response(text=avatar, content_type='image/svg+xml')
response.headers['Cache-Control'] = f'public, max-age={1337*42}'
response = web.Response(text=avatar, content_type="image/svg+xml")
response.headers["Cache-Control"] = f"public, max-age={1337*42}"
return response

View File

@ -1,35 +1,37 @@
# Written by retoor@molodetz.nl
# This code defines two classes, DocsHTMLView and DocsMDView, which are intended to asynchronously render HTML and Markdown templates respectively. Both classes inherit from the BaseView class.
# Dependencies: BaseView is imported from the "snek.system.view" package.
# MIT License
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Written by retoor@molodetz.nl
# This code defines two classes, DocsHTMLView and DocsMDView, which are intended to asynchronously render HTML and Markdown templates respectively. Both classes inherit from the BaseView class.
# Dependencies: BaseView is imported from the "snek.system.view" package.
# MIT License
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from snek.system.view import BaseView
class DocsHTMLView(BaseView):
async def get(self):
return await self.render_template("docs.html")
class DocsMDView(BaseView):
async def get(self):
return await self.render_template("docs.md")
return await self.render_template("docs.md")

View File

@ -1,6 +1,8 @@
from snek.system.view import BaseView
from aiohttp import web
from snek.system.view import BaseView
class DriveView(BaseView):
login_required = True
@ -14,21 +16,22 @@ class DriveView(BaseView):
drive_items = []
async for item in drive.items:
record = item.record
record['url'] = '/drive.bin/' + record['uid'] + '.' + item.extension
record["url"] = "/drive.bin/" + record["uid"] + "." + item.extension
drive_items.append(record)
return web.json_response(drive_items)
user = await self.services.user.get(uid=self.session.get("uid"))
drives = []
async for drive in self.services.drive.get_by_user(user['uid']):
async for drive in self.services.drive.get_by_user(user["uid"]):
record = drive.record
record['items'] = []
record["items"] = []
async for item in drive.items:
drive_item_record = item.record
drive_item_record['url'] = '/drive.bin/' + drive_item_record['uid'] + '.' + item.extension
record['items'].append(item.record)
drive_item_record["url"] = (
"/drive.bin/" + drive_item_record["uid"] + "." + item.extension
)
record["items"].append(item.record)
drives.append(record)
return web.json_response(drives)

View File

@ -12,6 +12,7 @@
from snek.system.view import BaseView
class IndexView(BaseView):
async def get(self):
return await self.render_template("index.html")
return await self.render_template("index.html")

View File

@ -7,9 +7,11 @@
# MIT License
from aiohttp import web
from snek.form.login import LoginForm
from snek.system.view import BaseFormView
class LoginView(BaseFormView):
form = LoginForm
@ -18,17 +20,23 @@ class LoginView(BaseFormView):
return web.HTTPFound("/web.html")
if self.request.path.endswith(".json"):
return await super().get()
return await self.render_template("login.html", {"form": await self.form(app=self.app).to_json()})
return await self.render_template(
"login.html", {"form": await self.form(app=self.app).to_json()}
)
async def submit(self, form):
if await form.is_valid:
user = await self.services.user.get(username=form['username'], deleted_at=None)
user = await self.services.user.get(
username=form["username"], deleted_at=None
)
await self.services.user.save(user)
self.session.update({
"logged_in": True,
"username": user['username'],
"uid": user["uid"],
"color": user["color"]
})
self.session.update(
{
"logged_in": True,
"username": user["username"],
"uid": user["uid"],
"color": user["color"],
}
)
return {"redirect_url": "/web.html"}
return {"is_valid": False}
return {"is_valid": False}

View File

@ -20,4 +20,4 @@ class LoginFormView(BaseFormView):
self.session["username"] = form.username.value
self.session["uid"] = form.uid.value
return {"redirect_url": "/web.html"}
return {"is_valid": False}
return {"is_valid": False}

View File

@ -15,10 +15,10 @@
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@ -29,6 +29,7 @@
from aiohttp import web
from snek.system.view import BaseView
@ -52,4 +53,4 @@ class LogoutView(BaseView):
del self.session["username"]
except KeyError:
pass
return await self.json_response({"redirect_url": self.redirect_url})
return await self.json_response({"redirect_url": self.redirect_url})

View File

@ -2,14 +2,16 @@
# This module defines a web view for user registration. It handles GET requests and form submissions for the registration process.
# The code makes use of 'RegisterForm' from 'snek.form.register' for handling registration forms and 'BaseFormView' from 'snek.system.view' for basic view functionalities.
# The code makes use of 'RegisterForm' from 'snek.form.register' for handling registration forms and 'BaseFormView' from 'snek.system.view' for basic view functionalities.
# MIT License
from aiohttp import web
from snek.form.register import RegisterForm
from snek.system.view import BaseFormView
class RegisterView(BaseFormView):
form = RegisterForm
@ -18,16 +20,20 @@ class RegisterView(BaseFormView):
return web.HTTPFound("/web.html")
if self.request.path.endswith(".json"):
return await super().get()
return await self.render_template("register.html", {"form": await self.form(app=self.app).to_json()})
return await self.render_template(
"register.html", {"form": await self.form(app=self.app).to_json()}
)
async def submit(self, form):
result = await self.app.services.user.register(
form.email.value, form.username.value, form.password.value
)
self.request.session.update({
"uid": result["uid"],
"username": result["username"],
"logged_in": True,
"color": result["color"]
})
return {"redirect_url": "/web.html"}
self.request.session.update(
{
"uid": result["uid"],
"username": result["username"],
"logged_in": True,
"color": result["color"],
}
)
return {"redirect_url": "/web.html"}

View File

@ -2,7 +2,7 @@
# This code defines a `RegisterFormView` class that handles the user registration process by using a form object, a view parent class, and asynchronously submitting the form data to register a user. It then stores the user's session details and provides a redirect URL to a specific page.
# Imports used but not part of the language:
# Imports used but not part of the language:
# snek.form.register.RegisterForm, snek.system.view.BaseFormView
# MIT License
@ -13,10 +13,10 @@
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@ -28,6 +28,7 @@
from snek.form.register import RegisterForm
from snek.system.view import BaseFormView
class RegisterFormView(BaseFormView):
form = RegisterForm
@ -35,10 +36,12 @@ class RegisterFormView(BaseFormView):
result = await self.app.services.user.register(
form.email.value, form.username.value, form.password.value
)
self.request.session.update({
"uid": result["uid"],
"username": result["username"],
"logged_in": True,
"color": result["color"]
})
return {"redirect_url": "/web.html"}
self.request.session.update(
{
"uid": result["uid"],
"username": result["username"],
"logged_in": True,
"color": result["color"],
}
)
return {"redirect_url": "/web.html"}

View File

@ -7,41 +7,44 @@
# 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 aiohttp import web
from snek.system.view import BaseView
import traceback
import json
from snek.system.model import now
import traceback
from aiohttp import web
from snek.system.model import now
from snek.system.profiler import Profiler
from snek.system.view import BaseView
class RPCView(BaseView):
class RPCApi:
def __init__(self, view, ws):
self.view = view
self.view = view
self.app = self.view.app
self.services = self.app.services
self.ws = ws
self.ws = ws
@property
def user_uid(self):
return self.view.session.get("uid")
@property
@property
def request(self):
return self.view.request
return self.view.request
def _require_login(self):
if not self.is_logged_in:
raise Exception("Not logged in")
@property
@property
def is_logged_in(self):
return self.view.session.get("logged_in", False)
async def mark_as_read(self, channel_uid):
self._require_login()
await self.services.channel_member.mark_as_read(channel_uid, self.user_uid)
await self.services.channel_member.mark_as_read(channel_uid, self.user_uid)
return True
async def login(self, username, password):
@ -54,16 +57,26 @@ class RPCView(BaseView):
self.view.session["username"] = user["username"]
self.view.session["user_nick"] = user["nick"]
record = user.record
del record['password']
del record['deleted_at']
await self.services.socket.add(self.ws,self.view.request.session.get('uid'))
async for subscription in self.services.channel_member.find(user_uid=self.view.request.session.get("uid"), deleted_at=None, is_banned=False):
await self.services.socket.subscribe(self.ws, subscription["channel_uid"], self.view.request.session.get("uid"))
return record
del record["password"]
del record["deleted_at"]
await self.services.socket.add(
self.ws, self.view.request.session.get("uid")
)
async for subscription in self.services.channel_member.find(
user_uid=self.view.request.session.get("uid"),
deleted_at=None,
is_banned=False,
):
await self.services.socket.subscribe(
self.ws,
subscription["channel_uid"],
self.view.request.session.get("uid"),
)
return record
async def search_user(self, query):
async def search_user(self, query):
self._require_login()
return [user['username'] for user in await self.services.user.search(query)]
return [user["username"] for user in await self.services.user.search(query)]
async def get_user(self, user_uid):
self._require_login()
@ -71,46 +84,56 @@ class RPCView(BaseView):
user_uid = self.user_uid
user = await self.services.user.get(uid=user_uid)
record = user.record
del record['password']
del record['deleted_at']
del record["password"]
del record["deleted_at"]
if user_uid != user["uid"]:
del record['email']
return record
del record["email"]
return record
async def get_messages(self, channel_uid, offset=0,timestamp = None):
async def get_messages(self, channel_uid, offset=0, timestamp=None):
self._require_login()
messages = []
for message in await self.services.channel_message.offset(channel_uid, offset or 0,timestamp or None):
extended_dict = await self.services.channel_message.to_extended_dict(message)
for message in await self.services.channel_message.offset(
channel_uid, offset or 0, timestamp or None
):
extended_dict = await self.services.channel_message.to_extended_dict(
message
)
messages.append(extended_dict)
return messages
async def get_channels(self):
self._require_login()
channels = []
async for subscription in self.services.channel_member.find(user_uid=self.user_uid, is_banned=False):
channel = await self.services.channel.get(uid=subscription['channel_uid'])
async for subscription in self.services.channel_member.find(
user_uid=self.user_uid, is_banned=False
):
channel = await self.services.channel.get(
uid=subscription["channel_uid"]
)
last_message = await channel.get_last_message()
color = None
color = None
if last_message:
last_message_user = await last_message.get_user()
color = last_message_user['color']
channels.append({
"name": subscription["label"],
"uid": subscription["channel_uid"],
"tag": channel["tag"],
"new_count": subscription["new_count"],
"is_moderator": subscription["is_moderator"],
"is_read_only": subscription["is_read_only"],
'new_count': subscription['new_count'],
'color': color
})
color = last_message_user["color"]
channels.append(
{
"name": subscription["label"],
"uid": subscription["channel_uid"],
"tag": channel["tag"],
"new_count": subscription["new_count"],
"is_moderator": subscription["is_moderator"],
"is_read_only": subscription["is_read_only"],
"new_count": subscription["new_count"],
"color": color,
}
)
return channels
async def send_message(self, channel_uid, message):
self._require_login()
await self.services.chat.send(self.user_uid, channel_uid, message)
return True
return True
async def echo(self, *args):
self._require_login()
@ -118,29 +141,47 @@ class RPCView(BaseView):
async def query(self, *args):
self._require_login()
query = args[0]
query = args[0]
lowercase = query.lower()
if any(keyword in lowercase for keyword in ["drop", "alter", "update", "delete", "replace", "insert", "truncate"]) and 'select' not in lowercase:
if (
any(
keyword in lowercase
for keyword in [
"drop",
"alter",
"update",
"delete",
"replace",
"insert",
"truncate",
]
)
and "select" not in lowercase
):
raise Exception("Not allowed")
records = [dict(record) async for record in self.services.channel.query(args[0])]
records = [
dict(record) async for record in self.services.channel.query(args[0])
]
for record in records:
try:
del record['email']
del record["email"]
except KeyError:
pass
pass
try:
del record["password"]
except KeyError:
pass
pass
try:
del record['message']
del record["message"]
except:
pass
try:
del record['html']
except:
del record["html"]
except:
pass
return [dict(record) async for record in self.services.channel.query(args[0])]
return [
dict(record) async for record in self.services.channel.query(args[0])
]
async def __call__(self, data):
try:
@ -150,30 +191,43 @@ class RPCView(BaseView):
raise Exception("Not allowed")
args = data.get("args") or []
if hasattr(super(), method_name) or not hasattr(self, method_name):
return await self._send_json({"callId": call_id, "data": "Not allowed"})
return await self._send_json(
{"callId": call_id, "data": "Not allowed"}
)
method = getattr(self, method_name.replace(".", "_"), None)
if not method:
raise Exception("Method not found")
success = True
success = True
try:
result = await method(*args)
except Exception as ex:
result = {"exception": str(ex), "traceback": traceback.format_exc()}
success = False
if result != "noresponse":
await self._send_json({"callId": call_id, "success": success, "data": result})
await self._send_json(
{"callId": call_id, "success": success, "data": result}
)
except Exception as ex:
print(str(ex), flush=True)
await self._send_json({"callId": call_id, "success": False, "data": str(ex)})
await self._send_json(
{"callId": call_id, "success": False, "data": str(ex)}
)
async def _send_json(self, obj):
await self.ws.send_str(json.dumps(obj, default=str))
async def get_online_users(self, channel_uid):
self._require_login()
return [dict(uid=record['uid'],username=record['username'], nick=record['nick'],last_ping=record['last_ping']) async for record in self.services.channel.get_online_users(channel_uid)]
return [
{
"uid": record["uid"],
"username": record["username"],
"nick": record["nick"],
"last_ping": record["last_ping"],
}
async for record in self.services.channel.get_online_users(channel_uid)
]
async def echo(self, obj):
await self.ws.send_json(obj)
@ -182,12 +236,20 @@ class RPCView(BaseView):
async def get_users(self, channel_uid):
self._require_login()
return [dict(uid=record['uid'],username=record['username'], nick=record['nick'],last_ping=record['last_ping']) async for record in self.services.channel.get_users(channel_uid)]
return [
{
"uid": record["uid"],
"username": record["username"],
"nick": record["nick"],
"last_ping": record["last_ping"],
}
async for record in self.services.channel.get_users(channel_uid)
]
async def ping(self, callId, *args):
if self.user_uid:
user = await self.services.user.get(uid=self.user_uid)
user['last_ping'] = now()
user["last_ping"] = now()
await self.services.user.save(user)
return {"pong": args}
@ -196,8 +258,14 @@ class RPCView(BaseView):
await ws.prepare(self.request)
if self.request.session.get("logged_in"):
await self.services.socket.add(ws, self.request.session.get("uid"))
async for subscription in self.services.channel_member.find(user_uid=self.request.session.get("uid"), deleted_at=None, is_banned=False):
await self.services.socket.subscribe(ws, subscription["channel_uid"], self.request.session.get("uid"))
async for subscription in self.services.channel_member.find(
user_uid=self.request.session.get("uid"),
deleted_at=None,
is_banned=False,
):
await self.services.socket.subscribe(
ws, subscription["channel_uid"], self.request.session.get("uid")
)
rpc = RPCView.RPCApi(self, ws)
async for msg in ws:
if msg.type == web.WSMsgType.TEXT:
@ -209,7 +277,7 @@ class RPCView(BaseView):
await self.services.socket.delete(ws)
break
elif msg.type == web.WSMsgType.ERROR:
pass
pass
elif msg.type == web.WSMsgType.CLOSE:
pass
pass
return ws

View File

@ -8,17 +8,17 @@
# - snek.system.view: Provides the BaseFormView class to facilitate form-related operations in a web context.
# MIT License
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@ -28,7 +28,6 @@
# SOFTWARE.
from aiohttp import web
from snek.form.search_user import SearchUserForm
from snek.system.view import BaseFormView
@ -40,14 +39,17 @@ class SearchUserView(BaseFormView):
users = []
query = self.request.query.get("query")
if query:
users = [user.record for user in await self.app.services.user.search(query)]
users = [user.record for user in await self.app.services.user.search(query)]
if self.request.path.endswith(".json"):
return await super().get()
current_user = await self.app.services.user.get(uid=self.session.get("uid"))
return await self.render_template("search_user.html", {"users": users, "query": query or '','current_user': current_user})
return await self.render_template(
"search_user.html",
{"users": users, "query": query or "", "current_user": current_user},
)
async def submit(self, form):
if await form.is_valid:
return {"redirect_url": "/search-user.html?query=" + form['username']}
return {"redirect_url": "/search-user.html?query=" + form["username"]}
return {"is_valid": False}

View File

@ -25,17 +25,18 @@
from snek.system.view import BaseView
class StatusView(BaseView):
async def get(self):
memberships = []
user = {}
user_id = self.session.get("uid")
if user_id:
user = await self.app.services.user.get(uid=user_id)
if not user:
return await self.json_response({"error": "User not found"}, status=404)
async for model in self.app.services.channel_member.find(
user_uid=user_id, deleted_at=None, is_banned=False
):
@ -69,4 +70,4 @@ class StatusView(BaseView):
self.app.cache.cache, None
),
}
)
)

View File

@ -1,15 +1,17 @@
from snek.system.view import BaseView
import aiohttp
import asyncio
from snek.system.terminal import TerminalSession
import pathlib
import aiohttp
from snek.system.terminal import TerminalSession
from snek.system.view import BaseView
class TerminalSocketView(BaseView):
login_required = True
user_sessions = {}
async def prepare_drive(self):
user = await self.services.user.get(uid=self.session.get("uid"))
root = pathlib.Path("drive").joinpath(user["uid"])
@ -19,40 +21,34 @@ class TerminalSocketView(BaseView):
destination_path = root.joinpath(path.name)
if not path.is_dir():
destination_path.write_bytes(path.read_bytes())
return root
async def get(self):
return root
async def get(self):
ws = aiohttp.web.WebSocketResponse()
await ws.prepare(self.request)
user = await self.services.user.get(uid=self.session.get("uid"))
root = await self.prepare_drive()
command = f"docker run -v ./{root}/:/root -it --memory 512M --cpus=0.5 -w /root snek_ubuntu /bin/bash"
session = self.user_sessions.get(user["uid"])
if not session:
self.user_sessions[user["uid"]] = TerminalSession(command=command)
session = self.user_sessions[user["uid"]]
session = self.user_sessions[user["uid"]]
await session.add_websocket(ws)
#asyncio.create_task(session.read_output(ws))
# asyncio.create_task(session.read_output(ws))
async for msg in ws:
if msg.type == aiohttp.WSMsgType.BINARY:
await session.write_input(msg.data.decode())
return ws
class TerminalView(BaseView):
login_required = True
async def get(self):
request = self.request
return await self.request.app.render_template('terminal.html',self.request)
return await self.request.app.render_template("terminal.html", self.request)

View File

@ -1,5 +1,6 @@
from snek.system.view import BaseView
class ThreadsView(BaseView):
async def get(self):
@ -12,22 +13,25 @@ class ThreadsView(BaseView):
if not last_message:
continue
thread["uid"] = channel['uid']
thread["uid"] = channel["uid"]
thread["name"] = await channel_member.get_name()
thread["new_count"] = channel_member["new_count"]
thread["last_message_on"] = channel["last_message_on"]
thread['created_at'] = thread['last_message_on']
thread["created_at"] = thread["last_message_on"]
thread['name_color'] = "#f05a28"
thread["name_color"] = "#f05a28"
thread["last_message_text"] = last_message["message"]
thread['last_message_user_uid'] = last_message["user_uid"]
user_last_message = await self.app.services.user.get(uid=last_message["user_uid"])
if channel['tag'] == "dm":
thread['name_color'] = user_last_message['color']
thread['last_message_user_color'] = user_last_message['color']
thread["last_message_user_uid"] = last_message["user_uid"]
user_last_message = await self.app.services.user.get(
uid=last_message["user_uid"]
)
if channel["tag"] == "dm":
thread["name_color"] = user_last_message["color"]
thread["last_message_user_color"] = user_last_message["color"]
threads.append(thread)
threads.sort(key=lambda x: x['last_message_on'] or '', reverse=True)
return await self.render_template("threads.html", dict(threads=threads,user=user))
threads.sort(key=lambda x: x["last_message_on"] or "", reverse=True)
return await self.render_template(
"threads.html", {"threads": threads, "user": user}
)

View File

@ -1,31 +1,36 @@
# Written by retoor@molodetz.nl
# This code defines a web application for uploading and retrieving files.
# It includes functionality to upload files through a POST request and retrieve them via a GET request.
# This code defines a web application for uploading and retrieving files.
# It includes functionality to upload files through a POST request and retrieve them via a GET request.
# The code uses the following non-standard imports:
# The code uses the following non-standard imports:
# - snek.system.view.BaseView: For extending view functionalities.
# - aiofiles: For asynchronous file operations.
# - aiohttp: For managing web server requests and responses.
# MIT License: This software is licensed under the MIT License, a permissive free software license.
from snek.system.view import BaseView
import aiofiles
import pathlib
from aiohttp import web
import uuid
import aiofiles
from aiohttp import web
from snek.system.view import BaseView
UPLOAD_DIR = pathlib.Path("./drive")
class UploadView(BaseView):
async def get(self):
uid = self.request.match_info.get("uid")
drive_item = await self.services.drive_item.get(uid)
response = web.FileResponse(drive_item["path"])
response.headers['Cache-Control'] = f'public, max-age={1337*420}'
response.headers['Content-Disposition'] = f'attachment; filename="{drive_item["name"]}"'
response.headers["Cache-Control"] = f"public, max-age={1337*420}"
response.headers["Content-Disposition"] = (
f'attachment; filename="{drive_item["name"]}"'
)
return response
async def post(self):
@ -36,7 +41,9 @@ class UploadView(BaseView):
channel_uid = None
drive = await self.services.drive.get_or_create(user_uid=self.request.session.get("uid"))
drive = await self.services.drive.get_or_create(
user_uid=self.request.session.get("uid")
)
extension_types = {
".jpg": "image",
@ -47,7 +54,7 @@ class UploadView(BaseView):
".mp3": "audio",
".pdf": "document",
".doc": "document",
".docx": "document"
".docx": "document",
}
while field := await reader.next():
@ -58,32 +65,45 @@ class UploadView(BaseView):
filename = field.filename
if not filename:
continue
name = str(uuid.uuid4()) + pathlib.Path(filename).suffix
file_path = pathlib.Path(UPLOAD_DIR).joinpath(name)
files.append(file_path)
async with aiofiles.open(str(file_path.absolute()), 'wb') as f:
async with aiofiles.open(str(file_path.absolute()), "wb") as f:
while chunk := await field.read_chunk():
await f.write(chunk)
drive_item = await self.services.drive_item.create(
drive["uid"], filename, str(file_path.absolute()), file_path.stat().st_size, file_path.suffix
drive["uid"],
filename,
str(file_path.absolute()),
file_path.stat().st_size,
file_path.suffix,
)
type_ = "unknown"
extension = "." + filename.split(".")[-1]
if extension in extension_types:
type_ = extension_types[extension]
extension_types[extension]
await self.services.drive_item.save(drive_item)
response = "Uploaded [" + filename + "](/drive.bin/" + drive_item["uid"] + ")"
#response = "<iframe width=\"100%\" frameborder=\"0\" allowfullscreen title=\"Embedded\" src=\"" + self.base_url + "/drive.bin/" + drive_item["uid"] + "\"></iframe>\n"
response = "[" + filename + "](/drive.bin/" + drive_item["uid"] + extension + ")"
response = (
"Uploaded [" + filename + "](/drive.bin/" + drive_item["uid"] + ")"
)
# response = "<iframe width=\"100%\" frameborder=\"0\" allowfullscreen title=\"Embedded\" src=\"" + self.base_url + "/drive.bin/" + drive_item["uid"] + "\"></iframe>\n"
response = (
"[" + filename + "](/drive.bin/" + drive_item["uid"] + extension + ")"
)
await self.services.chat.send(
self.request.session.get("uid"), channel_uid, response
)
return web.json_response({"message": "Files uploaded successfully", "files": [str(file) for file in files], "channel_uid": channel_uid})
return web.json_response(
{
"message": "Files uploaded successfully",
"files": [str(file) for file in files],
"channel_uid": channel_uid,
}
)

View File

@ -25,37 +25,55 @@
# SOFTWARE.
from aiohttp import web
from snek.system.view import BaseView
class WebView(BaseView):
login_required = True
async def get(self):
if self.login_required and not self.session.get("logged_in"):
return web.HTTPFound("/")
channel = await self.services.channel.get(uid=self.request.match_info.get("channel"))
channel = await self.services.channel.get(
uid=self.request.match_info.get("channel")
)
if not channel:
user = await self.services.user.get(uid=self.request.match_info.get("channel"))
user = await self.services.user.get(
uid=self.request.match_info.get("channel")
)
if user:
channel = await self.services.channel.get_dm(self.session.get("uid"), user["uid"])
channel = await self.services.channel.get_dm(
self.session.get("uid"), user["uid"]
)
if channel:
return web.HTTPFound("/channel/{}.html".format(channel["uid"]))
if not channel:
return web.HTTPNotFound()
channel_member = await self.app.services.channel_member.get(user_uid=self.session.get("uid"), channel_uid=channel["uid"])
channel_member = await self.app.services.channel_member.get(
user_uid=self.session.get("uid"), channel_uid=channel["uid"]
)
if not channel_member:
return web.HTTPNotFound()
channel_member['new_count'] = 0
channel_member["new_count"] = 0
await self.app.services.channel_member.save(channel_member)
user = await self.services.user.get(uid=self.session.get("uid"))
messages = [await self.app.services.channel_message.to_extended_dict(message) for message in await self.app.services.channel_message.offset(
channel["uid"]
)]
messages = [
await self.app.services.channel_message.to_extended_dict(message)
for message in await self.app.services.channel_message.offset(
channel["uid"]
)
]
for message in messages:
await self.app.services.notification.mark_as_read(self.session.get("uid"),message["uid"])
await self.app.services.notification.mark_as_read(
self.session.get("uid"), message["uid"]
)
name = await channel_member.get_name()
return await self.render_template("web.html", {"name": name, "channel": channel,"user": user,"messages": messages})
return await self.render_template(
"web.html",
{"name": name, "channel": channel, "user": user, "messages": messages},
)

View File

@ -1,7 +1,9 @@
import asyncio
import asyncssh
import os
import logging
import os
import asyncssh
asyncssh.set_debug_level(2)
logging.basicConfig(level=logging.DEBUG)
# Configuration for SFTP server
@ -11,6 +13,7 @@ PASSWORD = "woeii"
HOST = "localhost"
PORT = 2225
class MySFTPServer(asyncssh.SFTPServer):
def __init__(self, chan):
super().__init__(chan)
@ -31,8 +34,10 @@ class MySFTPServer(asyncssh.SFTPServer):
full_path = os.path.join(self.root, path.lstrip("/"))
return await super().listdir(full_path)
class MySSHServer(asyncssh.SSHServer):
"""Custom SSH server to handle authentication"""
def connection_made(self, conn):
print(f"New connection from {conn.get_extra_info('peername')}")
@ -46,11 +51,12 @@ class MySSHServer(asyncssh.SSHServer):
return True # Support password authentication
def validate_password(self, username, password):
print(username,password)
print(username, password)
return True
return username == USERNAME and password == PASSWORD
async def start_sftp_server():
os.makedirs(SFTP_ROOT, exist_ok=True) # Ensure the root directory exists
@ -59,11 +65,12 @@ async def start_sftp_server():
host=HOST,
port=PORT,
server_host_keys=["ssh_host_key"],
process_factory=MySFTPServer
process_factory=MySFTPServer,
)
print(f"SFTP server running on {HOST}:{PORT}")
await asyncio.Future() # Keep running forever
if __name__ == "__main__":
try:
asyncio.run(start_sftp_server())

View File

@ -1,7 +1,8 @@
import asyncio
import asyncssh
import os
import asyncssh
# SSH Server Configuration
HOST = "0.0.0.0"
PORT = 2225
@ -9,6 +10,7 @@ USERNAME = "user"
PASSWORD = "password"
SHELL = "/bin/sh" # Change to another shell if needed
class CustomSSHServer(asyncssh.SSHServer):
def connection_made(self, conn):
print(f"New connection from {conn.get_extra_info('peername')}")
@ -22,6 +24,7 @@ class CustomSSHServer(asyncssh.SSHServer):
def validate_password(self, username, password):
return username == USERNAME and password == PASSWORD
async def custom_bash_process(process):
"""Spawns a custom bash shell process"""
env = os.environ.copy()
@ -29,7 +32,12 @@ async def custom_bash_process(process):
# Start the Bash shell
bash_proc = await asyncio.create_subprocess_exec(
SHELL, "-i", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=env
SHELL,
"-i",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env=env,
)
async def read_output():
@ -48,6 +56,7 @@ async def custom_bash_process(process):
await asyncio.gather(read_output(), read_input())
async def start_ssh_server():
"""Starts the AsyncSSH server with Bash"""
await asyncssh.create_server(
@ -55,14 +64,14 @@ async def start_ssh_server():
host=HOST,
port=PORT,
server_host_keys=["ssh_host_key"],
process_factory=custom_bash_process
process_factory=custom_bash_process,
)
print(f"SSH server running on {HOST}:{PORT}")
await asyncio.Future() # Keep running
if __name__ == "__main__":
try:
asyncio.run(start_ssh_server())
except (OSError, asyncssh.Error) as e:
print(f"Error starting SSH server: {e}")

View File

@ -27,45 +27,48 @@
# The file ``ssh_user_ca`` must exist with a cert-authority entry of
# the certificate authority which can sign valid client certificates.
import asyncio, asyncssh, sys
import asyncio
import sys
import asyncssh
async def handle_client(process: asyncssh.SSHServerProcess) -> None:
width, height, pixwidth, pixheight = process.term_size
process.stdout.write(f'Terminal type: {process.term_type}, '
f'size: {width}x{height}')
process.stdout.write(
f"Terminal type: {process.term_type}, " f"size: {width}x{height}"
)
if pixwidth and pixheight:
process.stdout.write(f' ({pixwidth}x{pixheight} pixels)')
process.stdout.write('\nTry resizing your window!\n')
process.stdout.write(f" ({pixwidth}x{pixheight} pixels)")
process.stdout.write("\nTry resizing your window!\n")
while not process.stdin.at_eof():
try:
await process.stdin.read()
except asyncssh.TerminalSizeChanged as exc:
process.stdout.write(f'New window size: {exc.width}x{exc.height}')
process.stdout.write(f"New window size: {exc.width}x{exc.height}")
if exc.pixwidth and exc.pixheight:
process.stdout.write(f' ({exc.pixwidth}'
f'x{exc.pixheight} pixels)')
process.stdout.write('\n')
process.stdout.write(f" ({exc.pixwidth}" f"x{exc.pixheight} pixels)")
process.stdout.write("\n")
async def start_server() -> None:
await asyncssh.listen('', 2230, server_host_keys=['ssh_host_key'],
#authorized_client_keys='ssh_user_ca',
process_factory=handle_client)
await asyncssh.listen(
"",
2230,
server_host_keys=["ssh_host_key"],
# authorized_client_keys='ssh_user_ca',
process_factory=handle_client,
)
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(start_server())
except (OSError, asyncssh.Error) as exc:
sys.exit('Error starting server: ' + str(exc))
sys.exit("Error starting server: " + str(exc))
loop.run_forever()

View File

@ -24,32 +24,39 @@
# private key in it to use as a server host key. An SSH host certificate
# can optionally be provided in the file ``ssh_host_key-cert.pub``.
import asyncio, asyncssh, bcrypt, sys
import asyncio
import sys
from typing import Optional
passwords = {'guest': b'', # guest account with no password
'user': bcrypt.hashpw(b'user', bcrypt.gensalt()),
}
import asyncssh
import bcrypt
passwords = {
"guest": b"", # guest account with no password
"user": bcrypt.hashpw(b"user", bcrypt.gensalt()),
}
def handle_client(process: asyncssh.SSHServerProcess) -> None:
username = process.get_extra_info('username')
process.stdout.write(f'Welcome to my SSH server, {username}!\n')
#process.exit(0)
username = process.get_extra_info("username")
process.stdout.write(f"Welcome to my SSH server, {username}!\n")
# process.exit(0)
class MySSHServer(asyncssh.SSHServer):
def connection_made(self, conn: asyncssh.SSHServerConnection) -> None:
peername = conn.get_extra_info('peername')[0]
print(f'SSH connection received from {peername}.')
peername = conn.get_extra_info("peername")[0]
print(f"SSH connection received from {peername}.")
def connection_lost(self, exc: Optional[Exception]) -> None:
if exc:
print('SSH connection error: ' + str(exc), file=sys.stderr)
print("SSH connection error: " + str(exc), file=sys.stderr)
else:
print('SSH connection closed.')
print("SSH connection closed.")
def begin_auth(self, username: str) -> bool:
# If the user's password is the empty string, no auth is required
return passwords.get(username) != b''
return passwords.get(username) != b""
def password_auth_supported(self) -> bool:
return True
@ -60,18 +67,24 @@ class MySSHServer(asyncssh.SSHServer):
pw = passwords[username]
if not password and not pw:
return True
return bcrypt.checkpw(password.encode('utf-8'), pw)
return bcrypt.checkpw(password.encode("utf-8"), pw)
async def start_server() -> None:
await asyncssh.create_server(MySSHServer, '', 2231,
server_host_keys=['ssh_host_key'],
process_factory=handle_client)
await asyncssh.create_server(
MySSHServer,
"",
2231,
server_host_keys=["ssh_host_key"],
process_factory=handle_client,
)
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(start_server())
except (OSError, asyncssh.Error) as exc:
sys.exit('Error starting server: ' + str(exc))
sys.exit("Error starting server: " + str(exc))
loop.run_forever()

View File

@ -27,11 +27,15 @@
# The file ``ssh_user_ca`` must exist with a cert-authority entry of
# the certificate authority which can sign valid client certificates.
import asyncio, asyncssh, sys
import asyncio
import sys
from typing import List, cast
import asyncssh
class ChatClient:
_clients: List['ChatClient'] = []
_clients: List["ChatClient"] = []
def __init__(self, process: asyncssh.SSHServerProcess):
self._process = process
@ -40,8 +44,6 @@ class ChatClient:
async def handle_client(cls, process: asyncssh.SSHServerProcess):
await cls(process).run()
async def readline(self) -> str:
return cast(str, self._process.stdin.readline())
@ -53,54 +55,58 @@ class ChatClient:
if client != self:
client.write(msg)
def begin_auth(self, username: str) -> bool:
# If the user's password is the empty string, no auth is required
#return False
return True # passwords.get(username) != b''
# If the user's password is the empty string, no auth is required
# return False
return True # passwords.get(username) != b''
def password_auth_supported(self) -> bool:
return True
def validate_password(self, username: str, password: str) -> bool:
#if username not in passwords:
# if username not in passwords:
# return False
#pw = passwords[username]
#if not password and not pw:
# pw = passwords[username]
# if not password and not pw:
# return True
return True
#return bcrypt.checkpw(password.encode('utf-8'), pw)
# return bcrypt.checkpw(password.encode('utf-8'), pw)
async def run(self) -> None:
self.write('Welcome to chat!\n\n')
self.write("Welcome to chat!\n\n")
self.write('Enter your name: ')
name = (await self.readline()).rstrip('\n')
self.write("Enter your name: ")
name = (await self.readline()).rstrip("\n")
self.write(f'\n{len(self._clients)} other users are connected.\n\n')
self.write(f"\n{len(self._clients)} other users are connected.\n\n")
self._clients.append(self)
self.broadcast(f'*** {name} has entered chat ***\n')
self.broadcast(f"*** {name} has entered chat ***\n")
try:
async for line in self._process.stdin:
self.broadcast(f'{name}: {line}')
self.broadcast(f"{name}: {line}")
except asyncssh.BreakReceived:
pass
self.broadcast(f'*** {name} has left chat ***\n')
self.broadcast(f"*** {name} has left chat ***\n")
self._clients.remove(self)
async def start_server() -> None:
await asyncssh.listen('', 2235, server_host_keys=['ssh_host_key'],
process_factory=ChatClient.handle_client)
await asyncssh.listen(
"",
2235,
server_host_keys=["ssh_host_key"],
process_factory=ChatClient.handle_client,
)
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(start_server())
except (OSError, asyncssh.Error) as exc:
sys.exit('Error starting server: ' + str(exc))
sys.exit("Error starting server: " + str(exc))
loop.run_forever()