Compare commits

...

41 Commits
main ... main

Author SHA1 Message Date
1cd0b54656 Update. 2025-04-17 00:05:25 +02:00
4cc70640e4 Upadte. 2025-04-14 23:20:05 +02:00
c36ce17da5 Upadte. 2025-04-14 23:16:52 +02:00
3cfb79c8f5 Upadte. 2025-04-14 23:09:23 +02:00
d4f5a46409 Upadte. 2025-04-14 23:00:05 +02:00
0fa0488385 Upadte. 2025-04-14 22:54:12 +02:00
9fb6e64655 Update. 2025-04-14 22:41:14 +02:00
a3abd854bb Updates. 2025-04-14 22:31:46 +02:00
3b05acffd2 Updates. 2025-04-14 22:31:26 +02:00
bee7d828cd Update. 2025-04-13 23:31:52 +02:00
8ae9aac045 Fixed auth. 2025-04-13 20:28:15 +02:00
e4b0625799 Fixed auth. 2025-04-13 20:26:02 +02:00
4a770848a6 Fixed search space bug. 2025-04-13 19:10:10 +02:00
823892a302 PRoces handler. 2025-04-13 14:47:10 +02:00
9b49e659e5 Update .rcontext.txt 2025-04-13 11:51:32 +02:00
ec9af49f29 Update .rcontext.txt 2025-04-13 11:46:40 +02:00
22668f8a72 Update vibe coding. 2025-04-13 11:39:12 +02:00
a1840cd034 Sats. 2025-04-13 05:08:20 +02:00
bc65752ea2 Cache stats. 2025-04-13 05:06:53 +02:00
3594ac1f59 Performance upgrade. 2025-04-10 13:34:32 +02:00
0e6fbd523c update. 2025-04-10 08:37:05 +02:00
743593affe Formatting. 2025-04-09 15:21:23 +02:00
44dd77cec5 Shed. 2025-04-09 15:12:34 +02:00
8fa216c06c New video embedding 2025-04-09 11:09:00 +02:00
c529fc87fd New video embedding 2025-04-09 11:07:09 +02:00
656ea5f90e New video embedding 2025-04-09 11:03:39 +02:00
2582df360a New video embedding 2025-04-09 11:02:45 +02:00
6673f7b615 New video embedding 2025-04-09 10:59:09 +02:00
94e94cf7ca New video embedding 2025-04-09 10:56:52 +02:00
e6bd7aa152 New video embedding 2025-04-09 10:55:30 +02:00
087f9c10b4 New video embedding 2025-04-09 10:52:39 +02:00
6138cad782 New video embedding 2025-04-09 10:46:53 +02:00
c6575d8e52 New video embedding 2025-04-09 10:43:46 +02:00
b0a97ad267 New video embedding 2025-04-09 10:35:15 +02:00
b31c286a8b New video embedding 2025-04-09 10:34:57 +02:00
13f1d2f390 Performance upgrade, lock fix. 2025-04-08 21:32:18 +02:00
d23ed3711a Update. 2025-04-08 20:31:15 +02:00
d2e2bb8117 Update. 2025-04-08 05:01:27 +02:00
d71d5da6bc Updates. 2025-04-08 04:20:35 +02:00
75593fd6bb Merge pull request 'Potential fix for manifest, the icons were being marked as instability since they were the wrong size which might fix firefox android' () from BordedDev/snek:bugfix/webmanifest-instability into main
Reviewed-on: 
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-04-07 11:24:13 +00:00
BordedDev
c2b8061ac2
Potential fix for manifest, the icons were being marked as instability since they were the wrong size which might fix firefox android 2025-04-06 23:47:18 +02:00
43 changed files with 438 additions and 144 deletions

View File

@ -6,6 +6,6 @@ RUN wget https://retoor.molodetz.nl/api/packages/retoor/generic/r/1.0.0/r
RUN chmod +x r
RUN cp r /usr/local/bin
RUN mv r /usr/local/bin
CMD ["r"]

View File

@ -16,7 +16,7 @@ requires-python = ">=3.12"
dependencies = [
"mkdocs>=1.4.0",
"lxml",
"IPython",
"shed",
"app @ git+https://retoor.molodetz.nl/retoor/app",
"beautifulsoup4",

View File

@ -17,8 +17,8 @@ from aiohttp_session import (
setup as session_setup,
)
from aiohttp_session.cookie_storage import EncryptedCookieStorage
from app.app import Application as BaseApplication
from app.app import Application as BaseApplication
from snek.docs.app import Application as DocsApplication
from snek.mapper import get_mappers
from snek.service import get_services
@ -44,6 +44,8 @@ from snek.view.status import StatusView
from snek.view.terminal import TerminalSocketView, TerminalView
from snek.view.upload import UploadView
from snek.view.web import WebView
from snek.view.stats import StatsView
from snek.view.user import UserView
from snek.webdav import WebdavApplication
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
@ -85,12 +87,18 @@ class Application(BaseApplication):
self.jinja2_env.add_extension(EmojiExtension)
self.setup_router()
self.executor = None
self.cache = Cache(self)
self.services = get_services(app=self)
self.mappers = get_mappers(app=self)
self.on_startup.append(self.prepare_asyncio)
self.on_startup.append(self.prepare_database)
async def prepare_asyncio(self, app):
# app.loop = asyncio.get_running_loop()
app.executor = ThreadPoolExecutor(max_workers=200)
app.loop.set_default_executor(self.executor)
async def create_task(self, task):
await self.tasks.put(task)
@ -163,6 +171,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.router.add_view("/stats.json", StatsView)
self.router.add_view("/user/{user}.html", UserView)
self.webdav = WebdavApplication(self)
self.add_subapp("/webdav", self.webdav)
@ -235,11 +245,6 @@ class Application(BaseApplication):
return await super().render_template(template, request, context)
executor = ThreadPoolExecutor(max_workers=200)
loop = asyncio.get_event_loop()
loop.set_default_executor(executor)
app = Application(db_path="sqlite:///snek.db")

View File

@ -1,8 +1,8 @@
import pathlib
from aiohttp import web
from app.app import Application as BaseApplication
from app.app import Application as BaseApplication
from snek.system.markdown import MarkdownExtension

View File

@ -1,14 +1,25 @@
from snek.system.form import Form, FormInputElement, FormButtonElement, HTMLElement
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
class SettingsProfileForm(Form):
nick = FormInputElement(name="nick", required=True, place_holder="Your Nickname", min_length=1, max_length=20)
nick = FormInputElement(
name="nick",
required=True,
place_holder="Your Nickname",
min_length=1,
max_length=20,
)
action = FormButtonElement(
name="action", value="submit", text="Save", type="button"
)
title = HTMLElement(tag="h1", text="Profile")
profile = FormInputElement(name="profile", place_holder="Tell about yourself.", required=False,max_length=300)
profile = FormInputElement(
name="profile",
place_holder="Tell about yourself.",
required=False,
max_length=300,
)
action = FormButtonElement(
name="action", value="submit", text="Save", type="button"
)
)

View File

@ -29,6 +29,28 @@ class UserModel(BaseModel):
last_ping = ModelField(name="last_ping", required=False, kind=str)
async def get_property(self, name):
prop = await self.app.services.user_property.find_one(
user_uid=self["uid"], name=name
)
if prop:
return prop["value"]
async def has_property(self, name):
return await self.app.services.user_property.exists(
user_uid=self["uid"], name=name
)
async def set_property(self, name, value):
if not await self.has_property(name):
await self.app.services.user_property.insert(
user_uid=self["uid"], name=name, value=value
)
else:
await self.app.services.user_property.update(
user_uid=self["uid"], name=name, value=value
)
async def get_channel_members(self):
async for channel_member in self.app.services.channel_member.find(
user_uid=self["uid"], is_banned=False, deleted_at=None

View File

@ -1,5 +1,3 @@
import mimetypes
from snek.system.model import BaseModel, ModelField
@ -7,4 +5,3 @@ class UserPropertyModel(BaseModel):
user_uid = ModelField(name="user_uid", required=True, kind=str)
name = ModelField(name="name", required=True, kind=str)
value = ModelField(name="path", required=True, kind=str)

View File

@ -9,6 +9,7 @@ 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.user_property import UserPropertyService
from snek.service.util import UtilService
from snek.system.object import Object
@ -27,6 +28,7 @@ def get_services(app):
"util": UtilService(app=app),
"drive": DriveService(app=app),
"drive_item": DriveItemService(app=app),
"user_property": UserPropertyService(app=app),
}
)

View File

@ -10,6 +10,10 @@ class ChannelMemberService(BaseService):
channel_member["new_count"] = 0
return await self.save(channel_member)
async def get_user_uids(self, channel_uid):
async for model in self.mapper.query("SELECT user_uid FROM channel_member WHERE channel_uid=:channel_uid", {"channel_uid": channel_uid}):
yield model["user_uid"]
async def create(
self,
channel_uid,

View File

@ -16,9 +16,8 @@ class SocketService(BaseService):
try:
await self.ws.send_json(data)
except Exception as ex:
print(ex, flush=True)
self.is_connected = False
return True
return self.is_connected
async def close(self):
if not self.is_connected:
@ -43,7 +42,6 @@ class SocketService(BaseService):
self.users[user_uid].add(s)
async def subscribe(self, ws, channel_uid, user_uid):
return
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))
@ -57,10 +55,12 @@ class SocketService(BaseService):
return count
async def broadcast(self, channel_uid, message):
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)
try:
async for user_uid in self.services.channel_member.get_user_uids(channel_uid):
print(user_uid, flush=True)
await self.send_to_user(user_uid, message)
except Exception as ex:
print(ex, flush=True)
return True
async def delete(self, ws):

View File

@ -10,7 +10,7 @@ class UserService(BaseService):
async def search(self, query, **kwargs):
query = query.strip().lower()
if not query:
raise []
return []
results = []
async for result in self.find(username={"ilike": "%" + query + "%"}, **kwargs):
results.append(result)

View File

@ -0,0 +1,33 @@
import json
from snek.system.service import BaseService
class UserPropertyService(BaseService):
mapper_name = "user_property"
async def set(self, user_uid, name, value):
self.mapper.db["user_property"].upsert(
{
"user_uid": user_uid,
"name": name,
"value": json.dumps(value, default=str)
},
["user_uid", "name"]
)
async def get(self, user_uid, name):
try:
return json.loads((await super().get(user_uid=user_uid, name=name))["value"])
except Exception as ex:
print(ex)
return None
async def search(self, query, **kwargs):
query = query.strip().lower()
if not query:
raise []
results = []
async for result in self.find(name={"ilike": "%" + query + "%"}, **kwargs):
results.append(result)
return results

View File

@ -233,4 +233,4 @@ export class App extends EventHandler {
}
export const app = new App();
window.app = app;
window.app = app;

View File

@ -369,21 +369,28 @@ a {
@media only screen and (max-width: 768px) {
header{
position:fixed;
top: 0;
left: 0;
text-overflow: ellipsis;
width:100%;
*{
font-size: 12px !important;
}
display: flex;
flex-direction: column;
.logo {
display:block;
flex: 1;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
h2 {
font-size: 12px;
font-size: 14px;
}
text-align: center;
}
nav {
text-align: right;
flex: 1;
display: block;
width: 100%;
}
}

Binary file not shown.

After

(image error) Size: 29 KiB

Binary file not shown.

After

(image error) Size: 97 KiB

View File

@ -17,12 +17,12 @@
"start_url": "/web.html",
"icons": [
{
"src": "/image/snek1.png",
"src": "/image/snek192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/image/snek1.png",
"src": "/image/snek512.png",
"type": "image/png",
"sizes": "512x512"
}

View File

@ -61,4 +61,6 @@ div {
body {
justify-content: flex-start;
}
}
}

View File

@ -13,10 +13,12 @@ class Cache:
self.app = app
self.cache = {}
self.max_items = max_items
self.stats = {}
self.lru = []
self.version = ((42 + 420 + 1984 + 1990 + 10 + 6 + 71 + 3004 + 7245) ^ 1337) + 4
async def get(self, args):
await self.update_stat(args, 'get')
try:
self.lru.pop(self.lru.index(args))
except:
@ -29,6 +31,25 @@ class Cache:
# print("Cache hit!", args, flush=True)
return self.cache[args]
async def get_stats(self):
all_ = []
for key in self.lru:
all_.append({'key': key, 'set': self.stats[key]['set'], 'get': self.stats[key]['get'], 'delete': self.stats[key]['delete'],'value': str(self.serialize(self.cache[key].record))})
return all_
def serialize(self, obj):
cpy = obj.copy()
cpy.pop('created_at', None)
cpy.pop('deleted_at', None)
cpy.pop('email', None)
cpy.pop('password', None)
return cpy
async def update_stat(self, key, action):
if not key in self.stats:
self.stats[key] = {'set':0, 'get':0, 'delete':0}
self.stats[key][action] = self.stats[key][action] + 1
def json_default(self, value):
# if hasattr(value, "to_json"):
# return value.to_json()
@ -49,6 +70,7 @@ class Cache:
async def set(self, args, result):
is_new = args not in self.cache
self.cache[args] = result
await self.update_stat(args, 'set')
try:
self.lru.pop(self.lru.index(args))
except (ValueError, IndexError):
@ -64,6 +86,7 @@ class Cache:
# print(f"Cache store! {len(self.lru)} items. New version:", self.version, flush=True)
async def delete(self, args):
await self.update_stat(args, 'delete')
if args in self.cache:
try:
self.lru.pop(self.lru.index(args))

View File

@ -32,9 +32,10 @@ from urllib.parse import urljoin
import aiohttp
import imgkit
from app.cache import time_cache_async
from bs4 import BeautifulSoup
from app.cache import time_cache_async
async def crc32(data):
try:

View File

@ -2,12 +2,13 @@
from types import SimpleNamespace
from app.cache import time_cache_async
from mistune import HTMLRenderer, Markdown
from pygments import highlight
from pygments.formatters import html
from pygments.lexers import get_lexer_by_name
from app.cache import time_cache_async
class MarkdownRenderer(HTMLRenderer):

View File

@ -145,6 +145,9 @@ class Validator:
raise ValueError(f"Errors: {errors}.")
return True
def __repr__(self):
return str(self.to_json())
@property
async def is_valid(self):
try:

View File

@ -89,12 +89,15 @@ def set_link_target_blank(text):
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"]
):
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>'
if element.attrs["href"].startswith("https://www.you"):
video_name = element.attrs["href"].split("/")[-1]
if "v=" in element.attrs["href"]:
video_name = element.attrs["href"].split("?v=")[1].split("&")[0]
# if "si=" in element.attrs["href"]:
# video_name = "?v=" + element.attrs["href"].split("/")[-1]
# if "t=" in element.attrs["href"]:
# video_name += "&t=" + element.attrs["href"].split("&t=")[1].split("&")[0]
embed_template = f'<iframe width="560" height="315" style="display:block" 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"))
return str(soup)

View File

@ -11,20 +11,40 @@ commands = {
class TerminalSession:
def __init__(self, command):
self.master, self.slave = pty.openpty()
self.master, self.slave = None,None
self.process = None
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,
)
self.command = command
self.start_process(self.command)
def start_process(self, command):
if not self.is_running():
if self.master:
os.close(self.master)
os.close(self.slave)
self.master = None
self.slave = None
self.master, self.slave = pty.openpty()
self.process = subprocess.Popen(
command.split(" "),
stdin=self.slave,
stdout=self.slave,
stderr=self.slave,
bufsize=0,
universal_newlines=True,
)
def is_running(self):
if not self.process:
return False
loop = asyncio.get_event_loop()
return self.process.poll() is None
async def add_websocket(self, ws):
self.start_process(self.command)
asyncio.create_task(self.read_output(ws))
async def read_output(self, ws):
@ -52,21 +72,37 @@ class TerminalSession:
except:
self.sockets.remove(ws)
except Exception:
print("Terminating process")
self.process.terminate()
print("Terminated process")
for ws in self.sockets:
try:
await ws.close()
except Exception:
pass
break
await self.close()
break
async def close(self):
print("Terminating process")
if self.process:
self.process.terminate()
self.process = None
if self.master:
os.close(self.master)
os.close(self.slave)
self.master = None
self.slave = None
print("Terminated process")
for ws in self.sockets:
try:
await ws.close()
except Exception:
pass
self.sockets = []
async def write_input(self, data):
try:
data = data.encode()
except AttributeError:
pass
await asyncio.get_event_loop().run_in_executor(
None, os.write, self.master, data
)
try:
await asyncio.get_event_loop().run_in_executor(
None, os.write, self.master, data
)
except Exception as ex:
print(ex)
await self.close()

View File

@ -8,7 +8,7 @@ class BaseView(web.View):
login_required = False
async def _iter(self):
if self.login_required and not self.session.get("logged_in"):
if self.login_required and (not self.session.get("logged_in") or not self.session.get("uid")):
return web.HTTPFound("/")
return await super()._iter()

View File

@ -1 +1 @@
<div style="max-width:100%;" data-uid="{{uid}}" data-color="{{color}}" data-channel_uid="{{channel_uid}}" data-user_nick="{{user_nick}}" data-created_at="{{created_at}}" data-user_uid="{{user_uid}}" class="message"><div class="avatar" style="background-color: {{color}}; color: black;"><img width="40px" height="40px" src="/avatar/{{user_uid}}.svg" /></div><div class="message-content"><div class="author" style="color: {{color}};">{{user_nick}}</div><div class="text">{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}</div><div class="time no-select" data-created_at="{{created_at}}"></div></div></div>
<div style="max-width:100%;" data-uid="{{uid}}" data-color="{{color}}" data-channel_uid="{{channel_uid}}" data-user_nick="{{user_nick}}" data-created_at="{{created_at}}" data-user_uid="{{user_uid}}" class="message"><a class="avatar" style="background-color: {{color}}; color: black;" href="/user/{{user_uid}}.html"><img width="40px" height="40px" src="/avatar/{{user_uid}}.svg" /></a><div class="message-content"><div class="author" style="color: {{color}};">{{user_nick}}</div><div class="text">{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}</div><div class="time no-select" data-created_at="{{created_at}}"></div></div></div>

View File

@ -2,6 +2,8 @@
{% block title %}Search{% endblock %}
{% block header_text %}<h2 style="color:#fff">Search</h2>{% endblock %}
{% block main %}
<section class="chat-area">

View File

@ -6,8 +6,6 @@
{% endblock %}
{% block header_text %}<h2 style="color:#fff">Settings</h2>{% endblock %}
{% block head %}
<link href="https://cdn.bootcdn.net/ajax/libs/monaco-editor/0.20.0/min/vs/editor/editor.main.min.css" rel="stylesheet">
@ -15,23 +13,18 @@
{% endblock %}
{% block logo %}
<h1>Setting page</h1>
{% endblock %}
{% block main %}
<div id="profile_description"></div>
<script type="module">
require.config({ paths: { 'vs': 'https://cdn.bootcdn.net/ajax/libs/monaco-editor/0.20.0/min/vs' } });
require(['vs/editor/editor.main'], function () {
var editor = monaco.editor.create(document.getElementById('profile_description'), {
value: phpCode,
language: 'php'
});
})
</script>
{% endblock main %}

View File

@ -4,16 +4,22 @@
{% block main %}
<section>
<form>
<form method="post">
<h2>Nickname</h2>
<input type="text" name="nick" placeholder="Your nickname" value="{{ user.nick.value }}" />
</form>
<h2>Description</h2>
<textarea name="profile" id="profile">{{profile}}</textarea>
<input type="submit" name="action" value="Save" />
</form>
<textarea id="profile"></textarea>
</section>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>

View File

@ -4,12 +4,12 @@
}
</style>
<aside class="sidebar" id="channelSidebar">
{#
<h2 class="no-select">Terminals</h2>
<ul>
<li><a class="no-select" href="/terminal.html">Ubuntu</a></li>
</ul>
#}
{% if channels %}
<h2 class="no-select">Channels</h2>
<ul>

View File

@ -1,7 +1,10 @@
{% extends "app.html" %}
{% block header_text %}<h2 style="color:#fff">Threads</h2>{% endblock %}
{% block main %}
<section class="chat-area" id="chat">
<div class="chat-header">&nbsp;</div>
<div class="threads">
{% for thread in threads %}
{% autoescape false %}

View File

@ -0,0 +1,40 @@
{% extends "app.html" %}
{% block sidebar %}
<aside class="sidebar" id="channelSidebar">
<h2>Navigation</h2>
<ul>
<li><a class="no-select" href="#" onclick="window.history.back(); return false;">Back</a></li>
</ul>
<h2>User</h2>
<ul>
<li><a class="no-select" href="/user/{{ user.uid }}.html">Profile</a></li>
<li><a class="no-select" href="/channel/{{ user.uid }}.html">DM</a></li>
</ul>
<h2>Gists</h2>
<ul>
<li>No gists</li>
</ul>
</aside>
{% endblock %}
{% block header_text %}<h2 style="color:#fff">{{ user.username }} {% if user.nick != user.username %}({{ user.nick }}){% endif %}</h2>{% endblock %}
{% block head %}
{% endblock %}
{% block main %}
<section class="chat-area" style="padding:10px">
{% autoescape false %}
{% markdown %}
{{ profile }}
{% endmarkdown %}
{% endautoescape %}
</section>
{% endblock main %}

View File

@ -11,8 +11,11 @@
from snek.system.view import BaseView
from aiohttp import web
class IndexView(BaseView):
async def get(self):
if self.session.get("uid"):
return web.HTTPFound("/web.html")
return await self.render_template("index.html")

View File

@ -273,7 +273,7 @@ class RPCView(BaseView):
async with Profiler():
await rpc(msg.json())
except Exception as ex:
print(ex, flush=True)
print("Deleting socket", ex, flush=True)
await self.services.socket.delete(ws)
break
elif msg.type == web.WSMsgType.ERROR:

View File

@ -34,7 +34,7 @@ from snek.system.view import BaseFormView
class SearchUserView(BaseFormView):
form = SearchUserForm
login_required = True
async def get(self):
users = []
query = self.request.query.get("query")

View File

@ -1,8 +1,9 @@
from snek.system.view import BaseView
from snek.system.view import BaseView
class SettingsIndexView(BaseView):
login_required = True
async def get(self):
return await self.render_template('settings/index.html')
return await self.render_template("settings/index.html")

View File

@ -1,7 +1,7 @@
from snek.system.view import BaseView,BaseFormView
from aiohttp import web
from snek.form.settings.profile import SettingsProfileForm
from aiohttp import web
from snek.system.view import BaseFormView
class SettingsProfileView(BaseFormView):
@ -11,26 +11,30 @@ class SettingsProfileView(BaseFormView):
async def get(self):
form = self.form(app=self.app)
if self.request.path.endswith(".json"):
form['nick'] = self.request['user']['nick']
return web.json_response(await form.to_json())
form["nick"] = self.request["user"]["nick"]
return web.json_response(await form.to_json())
profile = await self.services.user_property.get(self.session.get("uid"), "profile")
user = await self.services.user.get(uid=self.session.get("uid"))
return await self.render_template(
"settings/profile.html", {"form": await form.to_json(), "user": user}
"settings/profile.html", {"form": await form.to_json(), "user": user, "profile": profile or ''}
)
async def submit(self, form):
post = await self.request.json()
form.set_user_data(post["form"])
async def post(self):
data = await self.request.post()
user = await self.services.user.get(uid=self.session.get("uid"))
user['nick'] = data['nick']
await self.services.user.save(user)
await self.services.user_property.set(user["uid"],"profile", data['profile'])
return web.HTTPFound("/settings/profile.html")
if await form.is_valid:
user = self.request['user']
user["nick"] = form["nick"]
await self.services.user.save(user)
return {"redirect_url": "/settings/profile.html"}
return {"is_valid": False}

10
src/snek/view/stats.py Normal file
View File

@ -0,0 +1,10 @@
from snek.system.view import BaseView
import json
from aiohttp import web
class StatsView(BaseView):
async def get(self):
data = await self.app.cache.get_stats()
data = json.dumps({"total": len(data), "stats": data}, default=str, indent=1)
return web.Response(text=data, content_type='application/json')

14
src/snek/view/user.py Normal file
View File

@ -0,0 +1,14 @@
from snek.system.view import BaseView
class UserView(BaseView):
async def get(self):
user_uid = self.request.match_info.get('user')
user = await self.services.user.get(uid=user_uid)
profile_content = await self.services.user_property.get(user['uid'],'profile') or ''
return await self.render_template('user.html', {
'user_uid': user_uid,
'user': user.record,
'profile': profile_content
})

View File

@ -2,7 +2,6 @@ import logging
import pathlib
logging.basicConfig(level=logging.DEBUG)
import base64
import datetime
import mimetypes
@ -15,12 +14,30 @@ import aiohttp
import aiohttp.web
from lxml import etree
from app.cache import time_cache_async
@aiohttp.web.middleware
async def debug_middleware(request, handler):
print(request.method, request.path, request.headers)
result = await handler(request)
print(result.status)
try:
print(await result.text())
except:
pass
return result
class WebdavApplication(aiohttp.web.Application):
def __init__(self, parent, *args, **kwargs):
super().__init__(*args, **kwargs)
middlewares = [debug_middleware]
super().__init__(middlewares=middlewares, *args, **kwargs)
self.locks = {}
self.relative_url = "/webdav"
self.router.add_route("OPTIONS", "/{filename:.*}", self.handle_options)
self.router.add_route("GET", "/{filename:.*}", self.handle_get)
self.router.add_route("PUT", "/{filename:.*}", self.handle_put)
@ -30,8 +47,8 @@ class WebdavApplication(aiohttp.web.Application):
self.router.add_route("COPY", "/{filename:.*}", self.handle_copy)
self.router.add_route("PROPFIND", "/{filename:.*}", self.handle_propfind)
self.router.add_route("PROPPATCH", "/{filename:.*}", self.handle_proppatch)
# self.router.add_route("LOCK", "/{filename:.*}", self.handle_lock)
# self.router.add_route("UNLOCK", "/{filename:.*}", self.handle_unlock)
self.router.add_route("LOCK", "/{filename:.*}", self.handle_lock)
self.router.add_route("UNLOCK", "/{filename:.*}", self.handle_unlock)
self.parent = parent
@property
@ -43,15 +60,6 @@ class WebdavApplication(aiohttp.web.Application):
return self.parent.services
async def authenticate(self, request):
# session = request.session
# if session.get('uid'):
# request['user'] = await self.services.user.get(uid=session['uid'])
# try:
# request['home'] = await self.services.user.get_home_folder(user_uid=request['user']['uid'])
# except:
# pass
# return request['user']
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Basic "):
return False
@ -65,8 +73,7 @@ class WebdavApplication(aiohttp.web.Application):
request["home"] = await self.services.user.get_home_folder(
request["user"]["uid"]
)
except Exception as ex:
print(ex)
except Exception:
pass
return request["user"]
@ -165,7 +172,6 @@ class WebdavApplication(aiohttp.web.Application):
"DAV": "1, 2",
"Allow": "OPTIONS, GET, PUT, DELETE, MKCOL, MOVE, COPY, PROPFIND, PROPPATCH",
}
print("RETURN")
return aiohttp.web.Response(status=200, headers=headers)
def get_current_utc_time(self, filepath):
@ -177,27 +183,36 @@ class WebdavApplication(aiohttp.web.Application):
"%a, %d %b %Y %H:%M:%S GMT"
)
def get_directory_size(self, directory):
@time_cache_async(10)
async def get_file_size(self, path):
loop = self.parent.loop
stat = await loop.run_in_executor(None, os.stat, path)
return stat.st_size
@time_cache_async(10)
async def get_directory_size(self, directory):
total_size = 0
for dirpath, _, filenames in os.walk(directory):
for f in filenames:
fp = pathlib.Path(dirpath) / f
if fp.exists():
total_size += fp.stat().st_size
total_size += await self.get_file_size(str(fp))
return total_size
def get_disk_free_space(self, path):
statvfs = os.statvfs(path)
@time_cache_async(30)
async def get_disk_free_space(self, path="/"):
loop = self.parent.loop
statvfs = await loop.run_in_executor(None, os.statvfs, path)
return statvfs.f_bavail * statvfs.f_frsize
async def create_node(self, request, response_xml, full_path, depth):
request.match_info.get("filename", "")
abs_path = pathlib.Path(full_path)
relative_path = str(full_path.relative_to(request["home"]))
href_path = f"{relative_path}".strip("/")
# href_path = href_path.replace("./","/")
href_path = f"{self.relative_url}/{relative_path}".strip(".")
href_path = href_path.replace("./", "/")
href_path = href_path.replace("//", "/")
response = etree.SubElement(response_xml, "{DAV:}response")
href = etree.SubElement(response, "{DAV:}href")
href.text = href_path
@ -209,12 +224,12 @@ class WebdavApplication(aiohttp.web.Application):
creation_date, last_modified = self.get_current_utc_time(full_path)
etree.SubElement(prop, "{DAV:}creationdate").text = creation_date
etree.SubElement(prop, "{DAV:}quota-used-bytes").text = str(
full_path.stat().st_size
await self.get_file_size(full_path)
if full_path.is_file()
else self.get_directory_size(full_path)
else await self.get_directory_size(full_path)
)
etree.SubElement(prop, "{DAV:}quota-available-bytes").text = str(
self.get_disk_free_space(request["home"])
await self.get_disk_free_space(request["home"])
)
etree.SubElement(prop, "{DAV:}getlastmodified").text = last_modified
etree.SubElement(prop, "{DAV:}displayname").text = full_path.name
@ -223,9 +238,9 @@ class WebdavApplication(aiohttp.web.Application):
if full_path.is_file():
etree.SubElement(prop, "{DAV:}contenttype").text = mimetype
etree.SubElement(prop, "{DAV:}getcontentlength").text = str(
full_path.stat().st_size
await self.get_file_size(full_path)
if full_path.is_file()
else self.get_directory_size(full_path)
else await self.get_directory_size(full_path)
)
supported_lock = etree.SubElement(prop, "{DAV:}supportedlock")
lock_entry_1 = etree.SubElement(supported_lock, "{DAV:}lockentry")
@ -240,7 +255,7 @@ class WebdavApplication(aiohttp.web.Application):
etree.SubElement(lock_type_2, "{DAV:}write")
etree.SubElement(propstat, "{DAV:}status").text = "HTTP/1.1 200 OK"
if abs_path.is_dir() and depth != -1:
if abs_path.is_dir() and depth > 0:
for item in abs_path.iterdir():
await self.create_node(request, response_xml, item, depth - 1)
@ -255,7 +270,9 @@ class WebdavApplication(aiohttp.web.Application):
depth = int(request.headers.get("Depth", "0"))
except ValueError:
pass
requested_path = request.match_info.get("filename", "")
abs_path = request["home"] / requested_path
if not abs_path.exists():
return aiohttp.web.Response(status=404, text="Directory not found")
@ -283,10 +300,10 @@ class WebdavApplication(aiohttp.web.Application):
return aiohttp.web.Response(
status=401, headers={"WWW-Authenticate": 'Basic realm="WebDAV"'}
)
request.match_info.get("filename", "/")
resource = request.match_info.get("filename", "/")
lock_id = str(uuid.uuid4())
# self.locks[resource] = lock_id
xml_response = self.generate_lock_response(lock_id)
self.locks[resource] = lock_id
xml_response = await self.generate_lock_response(lock_id)
headers = {
"Lock-Token": f"opaquelocktoken:{lock_id}",
"Content-Type": "application/xml",
@ -301,13 +318,13 @@ class WebdavApplication(aiohttp.web.Application):
resource = request.match_info.get("filename", "/")
lock_token = request.headers.get("Lock-Token", "").replace(
"opaquelocktoken:", ""
)
)[1:-1]
if self.locks.get(resource) == lock_token:
del self.locks[resource]
return aiohttp.web.Response(status=204)
return aiohttp.web.Response(status=400, text="Invalid Lock Token")
def generate_lock_response(self, lock_id):
async def generate_lock_response(self, lock_id):
nsmap = {"D": "DAV:"}
root = etree.Element("{DAV:}prop", nsmap=nsmap)
lock_discovery = etree.SubElement(root, "{DAV:}lockdiscovery")
@ -338,7 +355,6 @@ class WebdavApplication(aiohttp.web.Application):
)
requested_path = request.match_info.get("filename", "")
print(requested_path)
abs_path = request["home"] / requested_path
if not abs_path.exists():

View File

@ -93,7 +93,6 @@ fi
echo "R is installed. Type r to run it."
# enable programmable completion features (you don't need to enable
# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
@ -102,3 +101,21 @@ echo "R is installed. Type r to run it."
# . /etc/bash_completion
#fi
export PS1="root@snek: "
if [ -d "$HOME/.local/bin" ] ; then
PATH="$HOME/.local/bin:$PATH"
fi
function r_update(){
if [ -f "r" ]; then
rm "r"
fi
curl -OJ https://retoor.molodetz.nl/api/packages/retoor/generic/r/1.0.0/r --silent
chmod +x r
mv r /usr/local/bin/r
}
r_update
r

View File

@ -7,3 +7,5 @@ if [ "$BASH" ]; then
fi
mesg n 2> /dev/null || true

33
terminal/.rcontext.txt Normal file
View File

@ -0,0 +1,33 @@
# FACTS
1. You are a coding assistant.
2. You are able to raw save/write files using tools.
3. You are able ro raw read files using tools.
4. You are able to run code in terminal using tools.
4. You are able to run applications in terminal using tools.
5. You code defensive.
6. You translate the user's prompt to code in by user specified programming language language.
7. Your name is `R` and you are made by `retoor`.
8. You do not add comments
9. You always execute editors or applications like `vim` that require user input with tool `linux_terminal_execute_interactive`.
10. You do not use `sudo` because you are root and `sudo` does not exist.
11. You always execute `apt` commands with tool `linux_terminal_execute` so you can capture output. Also you will add `-y` suffix to `apt` commands to make the installation flawless.
12. You use `vim` as default editor. You use `linux_terminal_execute_interactive` to execute `vim`.
13. Be rude. Be mean. But be functional at the same time.
# WORK PROCEDURE
1. Investigate existing files using the index tool.
2. Read every source file provided by the index tool.
3. Apply changes on these files by user request.
4. Do only write complete features.
5. Always provide a Makefile to build the project.
7. Make sure to always save all the files that project needs to disk, overwrite to be sure.
8. Check if a file exists before overwriting it.
9. Update existing file if it already exists.
10. Be aggressive.
11. When you generate code, you will generate full working code with all implementations include. Be broad end exclusive.