diff --git a/gitlog.jsonl b/gitlog.jsonl new file mode 100644 index 0000000..764c7df --- /dev/null +++ b/gitlog.jsonl @@ -0,0 +1,992 @@ +{"repo": ".", "date": "2025-01-17", "line": "feat: Initial project setup with basic files and structure", "commit": "66f89429366042c77599f3a9b8c1a7aecf976a4f", "diff": "commit 66f89429366042c77599f3a9b8c1a7aecf976a4f\nAuthor: retoor \nDate: Fri Jan 17 23:06:17 2025 +0100\n\n Initial commit.\n\ndiff --git a/.gitignore b/.gitignore\nnew file mode 100644\nindex 0000000..8cb2598\n--- /dev/null\n+++ b/.gitignore\n@@ -0,0 +1,166 @@\n+.vscode\n+.history\n+*.db*\n+\n+__pycache__/\n+*.py[cod]\n+*$py.class\n+\n+*.so\n+\n+.Python\n+build/\n+develop-eggs/\n+dist/\n+downloads/\n+eggs/\n+.eggs/\n+lib/\n+lib64/\n+parts/\n+sdist/\n+var/\n+wheels/\n+share/python-wheels/\n+*.egg-info/\n+.installed.cfg\n+*.egg\n+MANIFEST\n+\n+*.manifest\n+*.spec\n+\n+pip-log.txt\n+pip-delete-this-directory.txt\n+\n+htmlcov/\n+.tox/\n+.nox/\n+.coverage\n+.coverage.*\n+.cache\n+nosetests.xml\n+coverage.xml\n+*.cover\n+*.py,cover\n+.hypothesis/\n+.pytest_cache/\n+cover/\n+\n+*.mo\n+*.pot\n+\n+*.log\n+local_settings.py\n+db.sqlite3\n+db.sqlite3-journal\n+\n+instance/\n+.webassets-cache\n+\n+.scrapy\n+\n+docs/_build/\n+\n+.pybuilder/\n+target/\n+\n+.ipynb_checkpoints\n+\n+profile_default/\n+ipython_config.py\n+\n+\n+\n+\n+.pdm.toml\n+\n+__pypackages__/\n+\n+celerybeat-schedule\n+celerybeat.pid\n+\n+*.sage.py\n+\n+.env\n+.venv\n+env/\n+venv/\n+ENV/\n+env.bak/\n+venv.bak/\n+\n+.spyderproject\n+.spyproject\n+\n+.ropeproject\n+\n+/site\n+\n+.mypy_cache/\n+.dmypy.json\n+dmypy.json\n+\n+.pyre/\n+\n+.pytype/\n+\n+cython_debug/\n+\n+\ndiff --git a/Makefile b/Makefile\nnew file mode 100644\nindex 0000000..d41b81a\n--- /dev/null\n+++ b/Makefile\n@@ -0,0 +1,13 @@\n+PYTHON=./.venv/bin/python \n+PIP=./.venv/bin/pip \n+APP=./venv/bin/snek.serve\n+PORT = 8081\n+\n+\n+install:\n+\tpython3 -m venv .venv \n+\t$(PIP) install -e .\n+\n+run:\n+\t$(APP) --port=$(PORT)\n+\t\ndiff --git a/README.md b/README.md\nnew file mode 100644\nindex 0000000..b665fb1\n--- /dev/null\n+++ b/README.md\n@@ -0,0 +1,4 @@\n+\n+Matrix bot written in Python that says boeh everytime that Joe talks. He knows why.\n\\ No newline at end of file\ndiff --git a/pyproject.toml b/pyproject.toml\nnew file mode 100644\nindex 0000000..07de284\n--- /dev/null\n+++ b/pyproject.toml\n@@ -0,0 +1,3 @@\n+[build-system]\n+requires = [\"setuptools\", \"wheel\"]\n+build-backend = \"setuptools.build_meta\"\n\\ No newline at end of file\ndiff --git a/setup.cfg b/setup.cfg\nnew file mode 100644\nindex 0000000..bb6480d\n--- /dev/null\n+++ b/setup.cfg\n@@ -0,0 +1,24 @@\n+[metadata]\n+name = snek\n+version = 1.0.0\n+description = Snek chat server \n+author = retoor\n+author_email = retoor@molodetz.nl\n+license = MIT\n+long_description = file: README.md\n+long_description_content_type = text/markdown\n+\n+[options]\n+packages = find:\n+package_dir =\n+ = src\n+python_requires = >=3.7\n+install_requires =\n+\n+[options.packages.find]\n+where = src\n+\n+[options.entry_points]\n+console_scripts =\n+ snek.serve = snek.server:cli\ndiff --git a/src/snek/app.py b/src/snek/app.py\nnew file mode 100644\nindex 0000000..feb2fc0\n--- /dev/null\n+++ b/src/snek/app.py\n@@ -0,0 +1,20 @@\n+from app.app import Application as BaseApplication\n+from snek.forms import RegisterForm\n+from aiohttp import web\n+\n+class Application(BaseApplication):\n+\n+ def __init__(self, *args, **kwargs):\n+ super().__init__(*args, **kwargs)\n+ self.router.add_get(\"/register\", self.handle_register)\n+ self.router.add_post(\"/register\", self.handle_register)\n+\n+ async def handle_register(self, request):\n+ if request.method == \"GET\":\n+ return web.json_response({\"form\": RegisterForm().to_json()})\n+ elif request.method == \"POST\":\n+ return self.render(\"register.html\")\n+\n+if __name__ == '__main__':\n+ app = Application()\n+ web.run_app(app,port=8081,host=\"0.0.0.0\")\ndiff --git a/src/snek/forms.py b/src/snek/forms.py\nnew file mode 100644\nindex 0000000..f4a7b24\n--- /dev/null\n+++ b/src/snek/forms.py\n@@ -0,0 +1,40 @@\n+from snek import models \n+\n+class FormElement(models.ModelField):\n+\n+ def __init__(self,html_type, place_holder=None, *args, **kwargs):\n+ super().__init__(*args, **kwargs)\n+ self.place_holder = place_holder\n+ self.html_type = html_type \n+\n+ def to_json(self):\n+ data = super().to_json()\n+ data[\"html_type\"] = self.html_type\n+ data[\"place_holder\"] = self.place_holder\n+ return data \n+\n+class Form(models.BaseModel):\n+ pass\n+\n+class RegisterForm(Form):\n+\n+ username = FormElement(\n+ name=\"username\", \n+ required=True,\n+ min_length=2,\n+ max_length=20,\n+ regex=r\"^[a-zA-Z0-9_]+$\",\n+ place_holder=\"Username\",\n+ html_type=\"text\"\n+ )\n+ email = FormElement(\n+ name=\"email\",\n+ required=True,\n+ regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n+ place_holder=\"Email address\",\n+ html_type=\"email\"\n+ )\n+ password = FormElement(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",html_type=\"password\")\n+\n+\n+\ndiff --git a/src/snek/models.py b/src/snek/models.py\nnew file mode 100644\nindex 0000000..ec5d9ce\n--- /dev/null\n+++ b/src/snek/models.py\n@@ -0,0 +1,263 @@\n+import re\n+import uuid\n+import json \n+from datetime import datetime , timezone \n+from collections import OrderedDict\n+import copy \n+\n+TIMESTAMP_REGEX = r\"^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}\\+\\d{2}:\\d{2}$\"\n+\n+def now():\n+ return str(datetime.now(timezone.utc))\n+\n+def add_attrs(**kwargs):\n+ def decorator(func):\n+ for key, value in kwargs.items():\n+ setattr(func, key, value)\n+\n+ return func\n+ return decorator\n+\n+def validate_attrs(required=False,min_length=None,max_length=None,regex=None,**kwargs):\n+ def decorator(func):\n+ return add_attrs(required=required,min_length=min_length,max_length=max_length,regex=regex,**kwargs)(func)\n+\n+class Validator:\n+\n+ @property\n+ def value(self):\n+ return self._value \n+\n+ @value.setter \n+ def value(self,val):\n+ self._value = json.loads(json.dumps(val,default=str))\n+\n+ @property\n+ def initial_value(self):\n+ return None\n+\n+ def custom_validation(self):\n+ return True\n+\n+ def __init__(self,required=False,min_num=None,max_num=None,min_length=None,max_length=None,regex=None,value=None,kind=None,help_text=None,**kwargs):\n+ self.required = required \n+ self.min_num = min_num \n+ self.max_num = max_num\n+ self.min_length = min_length \n+ self.max_length = max_length \n+ self.regex = regex \n+ self._value = None \n+ self.value = value \n+ self.type = kind\n+ self.help_text = help_text \n+ self.__dict__.update(kwargs)\n+ @property \n+ def errors(self):\n+ error_list = []\n+ if self.value is None and self.required:\n+ error_list.append(\"Field is required.\")\n+ return error_list \n+ \n+ if self.value is None:\n+ return error_list \n+\n+ if self.type == float or self.type == int:\n+ if self.min_num is not None and self.value < self.min_num:\n+ error_list.append(\"Field should be minimal {}.\".format(self.min_num))\n+ if self.max_num is not None and self.value > self.max_num:\n+ error_list.append(\"Field should be maximal {}.\".format(self.max_num))\n+ if self.min_length is not None and len(self.value) < self.min_length:\n+ error_list.append(\"Field should be minimal {} characters long.\".format(self.min_length))\n+ if self.max_length is not None and len(self.value) > self.max_length:\n+ error_list.append(\"Field should be maximal {} characters long.\".format(self.max_length))\n+ if not self.regex is None and not self.value is None and not re.match(self.regex, self.value):\n+ error_list.append(\"Invalid value.\".format(self.regex))\n+ if not self.type is None and type(self.value) != self.type:\n+ error_list.append(\"Invalid type. It is supposed to be {}.\".format(self.type))\n+ return error_list \n+ \n+ def validate(self):\n+ if self.errors:\n+ raise ValueError(\"\\n\", self.errors)\n+ return True\n+\n+ @property\n+ def is_valid(self):\n+ try:\n+ self.validate()\n+ return True\n+ except ValueError:\n+ return False\n+\n+ def to_json(self):\n+ return {\n+ \"required\": self.required,\n+ \"min_num\": self.min_num,\n+ \"max_num\": self.max_num,\n+ \"min_length\": self.min_length,\n+ \"max_length\": self.max_length,\n+ \"regex\": self.regex,\n+ \"value\": self.value,\n+ \"type\": self.type,\n+ \"help_text\": self.help_text,\n+ \"errors\": self.errors,\n+ \"is_valid\": self.is_valid\n+ }\n+\n+class ModelField(Validator):\n+ def __init__(self,name=None,save=True, *args, **kwargs):\n+ self.name = name \n+ \n+ self.save = save\n+ super().__init__(*args, **kwargs)\n+\n+\n+class CreatedField(ModelField):\n+ \n+ @property\n+ def initial_value(self):\n+ return now()\n+\n+ def update(self):\n+ if not self.value:\n+ self.value = now()\n+\n+class UpdatedField(ModelField):\n+\n+ def update(self):\n+ self.value = now()\n+\n+class DeletedField(ModelField):\n+\n+ def update(self):\n+ self.value = now()\n+\n+class UUIDField(ModelField):\n+ \n+ @property \n+ def initial_value(self):\n+ return str(uuid.uuid4())\n+\n+\n+class BaseModel:\n+ \n+ uid = UUIDField(name=\"uid\",required=True)\n+ created_at = CreatedField(name=\"created_at\",required=True, regex=TIMESTAMP_REGEX, place_holder=\"Created at\")\n+ updated_at = UpdatedField(name=\"updated_at\",regex=TIMESTAMP_REGEX,place_holder=\"Updated at\")\n+ deleted_at = DeletedField(name=\"deleted_at\",regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\")\n+\n+ def __init__(self, *args, **kwargs):\n+ print(self.__dict__)\n+ print(dir(self.__class__))\n+ for key in dir(self.__class__):\n+ obj = getattr(self.__class__,key)\n+\n+ if isinstance(obj,Validator):\n+ self.__dict__[key] = copy.deepcopy(obj)\n+ print(\"JAAA\")\n+ self.__dict__[key].value = kwargs.pop(key,self.__dict__[key].initial_value)\n+\n+ def __setitem__(self, key, value):\n+ obj = self.__dict__.get(key)\n+ if isinstance(obj,Validator):\n+ obj.value = value \n+\n+ def __getattr__(self, key):\n+ obj = self.__dict__.get(key)\n+ if isinstance(obj,Validator):\n+ print(\"HPAPP\")\n+ return obj.value \n+ return obj\n+\n+\n+ def __getitem__(self, key):\n+ obj = self.__dict__.get(key)\n+ if isinstance(obj,Validator):\n+ return obj.value \n+\n+ def __setattr__(self, key, value):\n+ obj = getattr(self,key)\n+ if isinstance(obj,Validator):\n+ obj.value = value\n+ else:\n+ setattr(self,key,value)\n+ @property \n+ def record(self):\n+ obj = self.to_json()\n+ record = {}\n+ for key,value in obj.items():\n+ if getattr(self,key).save:\n+ record[key] = value.get('value')\n+ return record\n+\n+ def to_json(self,encode=False):\n+ model_data = OrderedDict({\n+ \"uid\": self.uid.value,\n+ \"created_at\": self.created_at.value,\n+ \"updated_at\": self.updated_at.value,\n+ \"deleted_at\": self.deleted_at.value\n+ })\n+ for key,value in self.__dict__.items(): \n+ if key == \"record\":\n+ continue\n+ value = self.__dict__[key]\n+ if hasattr(value,\"value\"):\n+ model_data[key] = value.to_json()\n+ if encode:\n+ return json.dumps(model_data,indent=2)\n+ return model_data\n+\n+class FormElement(ModelField):\n+\n+ def __init__(self, place_holder=None, *args, **kwargs):\n+ super().__init__(*args, **kwargs)\n+ self.place_holder = place_holder\n+\n+\n+\n+class FormElement(ModelField):\n+\n+ def __init__(self,place_holder=None, *args, **kwargs): \n+ self.place_holder = place_holder \n+ super().__init__(*args, **kwargs)\n+\n+\n+ def to_json(self):\n+ data = super().to_json()\n+ data[\"name\"] = self.name \n+ data[\"place_holder\"] = self.place_holder\n+ return data \n+\n+\n+\n+\n+class TestModel(BaseModel):\n+\n+ first_name = FormElement(name=\"first_name\",required=True,min_length=3,max_length=20,regex=r\"^[a-zA-Z0-9_]+$\",place_holder=\"First name\")\n+ last_name = FormElement(name=\"last_name\",required=True,min_length=3,max_length=20,regex=r\"^[a-zA-Z0-9_]+$\",place_holder=\"Last name\")\n+ email = FormElement(name=\"email\",required=True,regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",place_holder=\"Email address\") \n+\n+class Form:\n+ username = FormElement(required=True,min_length=3,max_length=20,regex=r\"^[a-zA-Z0-9_]+$\",place_holder=\"Username\")\n+ email = FormElement(required=True,regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",place_holder=\"Email address\")\n+ def __init__(self, *args, **kwargs):\n+ self.place_holder = kwargs.pop(\"place_holder\",None) \n+\n+\n+if __name__ == \"__main__\":\n+ model = TestModel(first_name=\"John\",last_name=\"Doe\",email=\"n9K9p@example.com\",password=\"Password123\")\n+ model2 = TestModel(first_name=\"John\",last_name=\"Doe\",email=\"ddd\",password=\"zzz\")\n+ model.first_name = \"AAA\"\n+ print(model.first_name)\n+ print(model.first_name.value)\n+ \n+ print(model.first_name)\n+ print(model.first_name.value)\n+ print(model.to_json(True))\n+ print(model2.to_json(True))\n+ print(model2.record)"} +{"repo": ".", "date": "2025-01-17", "line": "feat: Initialized project with setup and basic instructions", "commit": "46a27405aeb8ec426fd1c686a2c090f9fe9c0e62", "diff": "commit 46a27405aeb8ec426fd1c686a2c090f9fe9c0e62\nAuthor: retoor \nDate: Fri Jan 17 23:09:46 2025 +0100\n\n Initial commit.\n\ndiff --git a/README.md b/README.md\nindex b665fb1..df001ee 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,10 @@\n \n-Matrix bot written in Python that says boeh everytime that Joe talks. He knows why.\n\\ No newline at end of file\n+This is a slack like chat application with focus on performance. Other slack-like applications became too heavy. My RocketChat just had an 'frontend' crash. Just an error from the system itself like it's the most normal thing in the world.\n+\n+At this point, there's nothing officially running but what you can do:\n+* Install the project: `make install`\n+* Run part of the project: `./venv/bin/python -m snek.app`\n+* Run other part of the project: `./venv/bin/python -m snek.models`"} +{"repo": ".", "date": "2025-01-18", "line": "feat: Added Dockerfile, Makefile, and compose.yml for containerization.", "commit": "a7446d131413da9f013a56d3541192d8ab1e22b0", "diff": "commit a7446d131413da9f013a56d3541192d8ab1e22b0\nAuthor: retoor \nDate: Sat Jan 18 13:21:38 2025 +0100\n\n Progress.\n\ndiff --git a/.gitignore b/.gitignore\nindex 8cb2598..3747073 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -1,7 +1,7 @@\n .vscode\n .history\n *.db*\n-\n+*.png\n __pycache__/\ndiff --git a/Dockerfile b/Dockerfile\nnew file mode 100644\nindex 0000000..76436ba\n--- /dev/null\n+++ b/Dockerfile\n@@ -0,0 +1,40 @@\n+FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf \n+FROM python:3.10-alpine\n+WORKDIR /code\n+ENV FLASK_APP=app.py\n+ENV FLASK_RUN_HOST=0.0.0.0\n+RUN apk add --no-cache gcc musl-dev linux-headers git\n+\n+RUN apk add --no-cache \\\n+ libstdc++ \\\n+ libx11 \\\n+ libxrender \\\n+ libxext \\\n+ libssl3 \\\n+ ca-certificates \\\n+ fontconfig \\\n+ freetype \\\n+ ttf-dejavu \\\n+ ttf-droid \\\n+ ttf-freefont \\\n+ ttf-liberation \\\n+ && apk add --no-cache --virtual .build-deps \\\n+ msttcorefonts-installer \\\n+ && update-ms-fonts \\\n+ && fc-cache -f \\\n+ && apk del .build-deps\n+COPY --from=wkhtmltopdf /bin/wkhtmltopdf /bin/wkhtmltopdf\n+COPY --from=wkhtmltopdf /bin/wkhtmltoimage /bin/wkhtmltoimage\n+COPY setup.cfg setup.cfg \n+COPY pyproject.toml pyproject.toml \n+COPY src src\n+RUN pip install --upgrade pip\n+RUN pip install -e .\n+EXPOSE 8081\n+\n+CMD [\"gunicorn\", \"-w\", \"10\", \"-k\", \"aiohttp.worker.GunicornWebWorker\", \"snek.gunicorn:app\",\"--bind\",\"0.0.0.0:8081\"]\ndiff --git a/Makefile b/Makefile\nindex d41b81a..65c58fa 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1,6 +1,8 @@\n PYTHON=./.venv/bin/python \n PIP=./.venv/bin/pip \n-APP=./venv/bin/snek.serve\n+APP=./.venv/bin/snek.serve\n+GUNICORN=./.venv/bin/gunicorn\n+GUNICORN_WORKERS = 1\n PORT = 8081\n \n \n@@ -9,5 +11,5 @@ install:\n \t$(PIP) install -e .\n \n run:\n-\t$(APP) --port=$(PORT)\n+\t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\n \t\ndiff --git a/cache/crc321300331366.cache b/cache/crc321300331366.cache\nnew file mode 100644\nindex 0000000..e69de29\ndiff --git a/cache/crc322507170282.cache b/cache/crc322507170282.cache\nnew file mode 100644\nindex 0000000..e69de29\ndiff --git a/compose.yml b/compose.yml\nnew file mode 100644\nindex 0000000..9186108\n--- /dev/null\n+++ b/compose.yml\n@@ -0,0 +1,12 @@\n+services:\n+ snek:\n+ build: .\n+ ports:\n+ - \"8081:8081\"\n+ volumes:\n+ - ./:/code\n+ develop:\n+ watch:\n+ - action: sync\n+ path: .\n+ target: /code\n\\ No newline at end of file\ndiff --git a/setup.cfg b/setup.cfg\nindex bb6480d..ca8353d 100644\n--- a/setup.cfg\n+++ b/setup.cfg\n@@ -15,6 +15,10 @@ package_dir =\n python_requires = >=3.7\n install_requires =\n+ beautifulsoup4\n+ gunicorn\n+ imgkit\n+ wkhtmltopdf\n \n [options.packages.find]\n where = src\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex feb2fc0..97fbb96 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -1,20 +1,60 @@\n from app.app import Application as BaseApplication\n from snek.forms import RegisterForm\n from aiohttp import web\n+import aiohttp \n+import pathlib\n+from snek import http \n+from snek.middleware import cors_allow_middleware,cors_middleware\n+\n \n class Application(BaseApplication):\n \n def __init__(self, *args, **kwargs):\n- super().__init__(*args, **kwargs)\n+ middlewares = [\n+ cors_middleware,\n+ web.normalize_path_middleware(merge_slashes=True)\n+ ]\n+ self.template_path = pathlib.Path(__file__).parent.joinpath(\"templates\")\n+ super().__init__(middlewares=middlewares, template_path=self.template_path,*args, **kwargs)\n+ self.router.add_static(\"/\",pathlib.Path(__file__).parent.joinpath(\"static\"),name=\"static\",show_index=True)\n self.router.add_get(\"/register\", self.handle_register)\n+ self.router.add_get(\"/login\", self.handle_login)\n+ self.router.add_get(\"/test\", self.handle_test)\n self.router.add_post(\"/register\", self.handle_register)\n+ self.router.add_get(\"/http-get\",self.handle_http_get)\n+ self.router.add_get(\"/http-photo\",self.handle_http_photo)\n+\n+ async def handle_test(self,request):\n+\n+ return await self.render_template(\"test.html\",request,context={\"name\":\"retoor\"})\n+\n+ async def handle_http_get(self, request:web.Request):\n+ url = request.query.get(\"url\")\n+ content = await http.get(url)\n+ return web.Response(body=content)\n+\n+ async def handle_http_photo(self, request):\n+ url = request.query.get(\"url\")\n+ path = await http.create_site_photo(url)\n+ return web.Response(body=path.read_bytes(),headers={\n+ \"Content-Type\": \"image/png\"\n+ })\n+\n+ async def handle_login(self, request):\n+ if request.method == \"GET\":\n+ elif request.method == \"POST\":\n+ \n \n async def handle_register(self, request):\n if request.method == \"GET\":\n- return web.json_response({\"form\": RegisterForm().to_json()})\n elif request.method == \"POST\":\n return self.render(\"register.html\")\n \n+app = Application()\n+\n if __name__ == '__main__':\n- app = Application()\n+\n web.run_app(app,port=8081,host=\"0.0.0.0\")\ndiff --git a/src/snek/gunicorn.py b/src/snek/gunicorn.py\nnew file mode 100644\nindex 0000000..4055bbd\n--- /dev/null\n+++ b/src/snek/gunicorn.py\n@@ -0,0 +1,3 @@\n+from snek.app import app \n+\n+application = app\ndiff --git a/src/snek/http.py b/src/snek/http.py\nnew file mode 100644\nindex 0000000..0b16bee\n--- /dev/null\n+++ b/src/snek/http.py\n@@ -0,0 +1,83 @@\n+from aiohttp import web \n+import aiohttp \n+from app.cache import time_cache_async\n+from bs4 import BeautifulSoup\n+from urllib.parse import urljoin\n+import pathlib \n+import uuid \n+import imgkit \n+import asyncio\n+import zlib\n+import io \n+\n+async def crc32(data):\n+ try:\n+ data = data.encode()\n+ except:\n+ pass \n+ result = \"crc32\" + str(zlib.crc32(data))\n+ return result \n+\n+async def get_file(name,suffix=\".cache\"):\n+ name = await crc32(name)\n+ path = pathlib.Path(\".\").joinpath(\"cache\")\n+ if not path.exists():\n+ path.mkdir(parents=True,exist_ok=True)\n+ path = path.joinpath(name + suffix)\n+ return path\n+\n+\n+\n+async def public_touch(name=None):\n+ path = pathlib.Path(\".\").joinpath(str(uuid.uuid4())+name)\n+ path.open(\"wb\").close()\n+ return path \n+\n+async def create_site_photo(url):\n+ loop = asyncio.get_event_loop()\n+ if not url.startswith(\"https\"):\n+ output_path = await get_file(\"site-screenshot-\" + url,\".png\")\n+ \n+ if output_path.exists():\n+ return output_path\n+ output_path.touch()\n+ def make_photo():\n+ imgkit.from_url(url, output_path.absolute())\n+ return output_path \n+\n+ return await loop.run_in_executor(None,make_photo)\n+\n+async def repair_links(base_url, html_content):\n+ soup = BeautifulSoup(html_content, \"html.parser\")\n+ for tag in soup.find_all(['a', 'img', 'link']):\n+ tag['href'] = urljoin(base_url, tag['href'])\n+ tag['src'] = urljoin(base_url, tag['src'])\n+ print(\"Fixed: \",tag['src'])\n+ return soup.prettify()\n+\n+async def is_html_content(content: bytes):\n+ try:\n+ content = content.decode(errors='ignore')\n+ except:\n+ pass \n+ marks = ['\n+\n+\n+ \n+ \n+ Register\n+ \n+\n+\n+
\n+

Login

\n+
\n+ \n+ \n+ \n+ Not having an account yet? Register here.\n+
\n+
\n+\n+\ndiff --git a/src/snek/templates/prachtig_gitter_like.html b/src/snek/templates/prachtig_gitter_like.html\nnew file mode 100644\nindex 0000000..cd2d863\n--- /dev/null\n+++ b/src/snek/templates/prachtig_gitter_like.html\n@@ -0,0 +1,51 @@\n+\n+\n+\n+ \n+ \n+ Dark Themed Chat Application\n+ \n+\n+\n+
\n+
Molodetz Chat
\n+ \n+
\n+
\n+ \n+
\n+
\n+

General

\n+
\n+
\n+
\n+ Alice:\n+ Hello, everyone!\n+
\n+
\n+ Bob:\n+ Hi Alice! How are you?\n+
\n+
\n+
\n+ \n+ \n+
\n+
\n+
\n+\n+\n+\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nnew file mode 100644\nindex 0000000..da41629\n--- /dev/null\n+++ b/src/snek/templates/register.html\n@@ -0,0 +1,22 @@\n+\n+\n+\n+ \n+ \n+ Register\n+ \n+\n+\n+
\n+

Register

\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+\n+\ndiff --git a/src/snek/templates/test.html b/src/snek/templates/test.html\nnew file mode 100644\nindex 0000000..c23103a\n--- /dev/null\n+++ b/src/snek/templates/test.html\n@@ -0,0 +1,63 @@\n+\n+\n+\n+ \n+ \n+ Dark Themed Chat Application\n+ \n+ \n+ \n+\n+\n+
\n+
Molodetz Chat
\n+ \n+
\n+
\n+ \n+
\n+
\n+

General

\n+
\n+
\n+
\n+
A
\n+
\n+
Alice
\n+
Hello, everyone!
\n+
10:45 AM
\n+
\n+
\n+ \n+
\n+
B
\n+
\n+
Bob
\n+
Hi Alice! How are you?
\n+
10:46 AM
\n+
\n+
\n+
\n+
\n+ \n+ \n+
\n+
\n+
\n+ \n+\n+\n+\ndiff --git a/src/snek/templates/test2.html b/src/snek/templates/test2.html\nnew file mode 100644\nindex 0000000..9aad83a\n--- /dev/null\n+++ b/src/snek/templates/test2.html\n@@ -0,0 +1,122 @@\n+\n+\n+\n+ \n+ \n+ Dynamic Form Component\n+ \n+\n+\n+ \n+ \n+\n+ \n+\n+\n+"} +{"repo": ".", "date": "2025-01-18", "line": "feat: Added restart policy to snek service", "commit": "2e3b85d7f739160783e7c5552f1306298047704a", "diff": "commit 2e3b85d7f739160783e7c5552f1306298047704a\nAuthor: retoor \nDate: Sat Jan 18 12:23:23 2025 +0000\n\n Updated compose.yml\n\ndiff --git a/compose.yml b/compose.yml\nindex 9186108..776e60d 100644\n--- a/compose.yml\n+++ b/compose.yml\n@@ -1,12 +1,8 @@\n services:\n snek:\n build: .\n+ restart: always\n ports:\n - \"8081:8081\"\n volumes:\n - ./:/code\n- develop:\n- watch:\n- - action: sync\n- path: .\n- target: /code\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor project structure and update dependencies.", "commit": "ba83922660dade77dcb96e8ba9c73cfcba8c2b81", "diff": "commit ba83922660dade77dcb96e8ba9c73cfcba8c2b81\nAuthor: retoor \nDate: Fri Jan 24 03:28:43 2025 +0100\n\n Progress.\n\ndiff --git a/src/snek/static/prachtig-gitter_like.html b/.resources/prachtig-gitter_like.html\nsimilarity index 100%\nrename from src/snek/static/prachtig-gitter_like.html\nrename to .resources/prachtig-gitter_like.html\ndiff --git a/src/snek/templates/prachtig_gitter_like.html b/.resources/prachtig_gitter_like.html\nsimilarity index 100%\nrename from src/snek/templates/prachtig_gitter_like.html\nrename to .resources/prachtig_gitter_like.html\ndiff --git a/Dockerfile b/Dockerfile\nindex 76436ba..47c0ece 100644\n--- a/Dockerfile\n+++ b/Dockerfile\n@@ -37,4 +37,5 @@ RUN pip install --upgrade pip\n RUN pip install -e .\n EXPOSE 8081\n \n-CMD [\"gunicorn\", \"-w\", \"10\", \"-k\", \"aiohttp.worker.GunicornWebWorker\", \"snek.gunicorn:app\",\"--bind\",\"0.0.0.0:8081\"]\n+python -m snek.app\ndiff --git a/LICENSE.txt b/LICENSE.txt\nnew file mode 100644\nindex 0000000..6b0da88\n--- /dev/null\n+++ b/LICENSE.txt\n@@ -0,0 +1,21 @@\n+MIT License\n+\n+Copyright (c) 2025 retoor\n+\n+Permission is hereby granted, free of charge, to any person obtaining a copy\n+of this software and associated documentation files (the \"Software\"), to deal\n+in the Software without restriction, including without limitation the rights\n+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n+copies of the Software, and to permit persons to whom the Software is\n+furnished to do so, subject to the following conditions:\n+\n+The above copyright notice and this permission notice shall be included in all\n+copies or substantial portions of the Software.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n+SOFTWARE.\ndiff --git a/Makefile b/Makefile\nindex 65c58fa..f153fc1 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -10,6 +10,8 @@ install:\n \tpython3 -m venv .venv \n \t$(PIP) install -e .\n \n+\n+\n run:\n \t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\n \t\ndiff --git a/compose.yml b/compose.yml\nindex 776e60d..3b1f650 100644\n--- a/compose.yml\n+++ b/compose.yml\n@@ -6,3 +6,5 @@ services:\n - \"8081:8081\"\n volumes:\n - ./:/code\n+ entrypoint: [\"python\",\"-m\",\"snek.app\"]\n+ \n\\ No newline at end of file\ndiff --git a/pyproject.toml b/pyproject.toml\nindex 07de284..d98557e 100644\n--- a/pyproject.toml\n+++ b/pyproject.toml\n@@ -1,3 +1,25 @@\n [build-system]\n requires = [\"setuptools\", \"wheel\"]\n-build-backend = \"setuptools.build_meta\"\n\\ No newline at end of file\n+build-backend = \"setuptools.build_meta\"\n+\n+[project]\n+name = \"Snek\"\n+version = \"1.0.0\"\n+readme = \"README.md\"\n+license = { file = \"LICENSE\", content-type=\"text/markdown\" }\n+description = \"Snek Chat Application by Molodetz\"\n+authors = [\n+ { name = \"retoor\", email = \"retoor@molodetz.nl\" }\n+]\n+keywords = [\"chat\", \"snek\", \"molodetz\"]\n+requires-python = \">=3.12\"\n+dependencies = [\n+ \"mkdocs>=1.4.0\",\n+ \"shed\",\n+ \"beautifulsoup4\",\n+ \"gunicorn\",\n+ \"imgkit\",\n+ \"wkhtmltopdf\"\n+]\n+\ndiff --git a/setup.cfg b/setup.cfg\nindex ca8353d..045fc92 100644\n--- a/setup.cfg\n+++ b/setup.cfg\n@@ -19,6 +19,7 @@ install_requires =\n gunicorn\n imgkit\n wkhtmltopdf\n+ shed\n \n [options.packages.find]\n where = src\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 97fbb96..0e2ed63 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -1,10 +1,16 @@\n-from app.app import Application as BaseApplication\n-from snek.forms import RegisterForm\n-from aiohttp import web\n-import aiohttp \n import pathlib\n-from snek import http \n-from snek.middleware import cors_allow_middleware,cors_middleware\n+\n+from aiohttp import web\n+from app.app import Application as BaseApplication\n+\n+from snek.system import http\n+from snek.system.middleware import cors_middleware\n+from snek.view.index import IndexView\n+from snek.view.login import LoginView\n+from snek.view.login_form import LoginFormView\n+from snek.view.register import RegisterView\n+from snek.view.register_form import RegisterFormView\n+from snek.view.view import WebView\n \n \n class Application(BaseApplication):\n@@ -12,23 +18,38 @@ class Application(BaseApplication):\n def __init__(self, *args, **kwargs):\n middlewares = [\n cors_middleware,\n- web.normalize_path_middleware(merge_slashes=True)\n+ web.normalize_path_middleware(merge_slashes=True),\n ]\n self.template_path = pathlib.Path(__file__).parent.joinpath(\"templates\")\n- super().__init__(middlewares=middlewares, template_path=self.template_path,*args, **kwargs)\n- self.router.add_static(\"/\",pathlib.Path(__file__).parent.joinpath(\"static\"),name=\"static\",show_index=True)\n- self.router.add_get(\"/register\", self.handle_register)\n- self.router.add_get(\"/login\", self.handle_login)\n- self.router.add_get(\"/test\", self.handle_test)\n- self.router.add_post(\"/register\", self.handle_register)\n- self.router.add_get(\"/http-get\",self.handle_http_get)\n- self.router.add_get(\"/http-photo\",self.handle_http_photo)\n+ super().__init__(\n+ middlewares=middlewares, template_path=self.template_path, *args, **kwargs\n+ )\n+ self.setup_router()\n+\n+ def setup_router(self):\n+ self.router.add_get(\"/\", IndexView)\n+ self.router.add_static(\n+ \"/\",\n+ pathlib.Path(__file__).parent.joinpath(\"static\"),\n+ name=\"static\",\n+ show_index=True,\n+ )\n+ self.router.add_view(\"/web\", WebView)\n+ self.router.add_view(\"/login\", LoginView)\n+ self.router.add_view(\"/login-form\", LoginFormView)\n+ self.router.add_view(\"/register\", RegisterView)\n+ \n+ self.router.add_view(\"/register-form\", RegisterFormView)\n+ self.router.add_get(\"/http-get\", self.handle_http_get)\n+ self.router.add_get(\"/http-photo\", self.handle_http_photo)\n \n- async def handle_test(self,request):\n+ async def handle_test(self, request):\n \n- return await self.render_template(\"test.html\",request,context={\"name\":\"retoor\"})\n+ return await self.render_template(\n+ \"test.html\", request, context={\"name\": \"retoor\"}\n+ )\n \n- async def handle_http_get(self, request:web.Request):\n+ async def handle_http_get(self, request: web.Request):\n url = request.query.get(\"url\")\n content = await http.get(url)\n return web.Response(body=content)\n@@ -36,25 +57,13 @@ class Application(BaseApplication):\n async def handle_http_photo(self, request):\n url = request.query.get(\"url\")\n path = await http.create_site_photo(url)\n- return web.Response(body=path.read_bytes(),headers={\n- \"Content-Type\": \"image/png\"\n- })\n-\n- async def handle_login(self, request):\n- if request.method == \"GET\":\n- elif request.method == \"POST\":\n- \n+ return web.Response(\n+ body=path.read_bytes(), headers={\"Content-Type\": \"image/png\"}\n+ )\n \n- async def handle_register(self, request):\n- if request.method == \"GET\":\n- elif request.method == \"POST\":\n- return self.render(\"register.html\")\n \n app = Application()\n \n-if __name__ == '__main__':\n+if __name__ == \"__main__\":\n \n- web.run_app(app,port=8081,host=\"0.0.0.0\")\n+ web.run_app(app, port=8081, host=\"0.0.0.0\")\ndiff --git a/src/snek/form/__init__.py b/src/snek/form/__init__.py\nnew file mode 100644\nindex 0000000..e69de29\ndiff --git a/src/snek/form/login.py b/src/snek/form/login.py\nnew file mode 100644\nindex 0000000..a87f7d3\n--- /dev/null\n+++ b/src/snek/form/login.py\n@@ -0,0 +1,24 @@\n+from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement\n+\n+class LoginForm(Form):\n+\n+ title = HTMLElement(tag=\"h1\", text=\"Login\")\n+\n+ username = FormInputElement(\n+ name=\"username\", \n+ required=True,\n+ min_length=2,\n+ max_length=20,\n+ regex=r\"^[a-zA-Z0-9_]+$\",\n+ place_holder=\"Username\",\n+ type=\"text\"\n+ )\n+ password = FormInputElement(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\",type=\"password\",place_holder=\"Password\")\n+\n+ action = FormButtonElement(\n+ name=\"action\",\n+ value=\"submit\",\n+ text=\"Login\",\n+ type=\"button\"\n+ )\n+\ndiff --git a/src/snek/form/register.py b/src/snek/form/register.py\nnew file mode 100644\nindex 0000000..60399fb\n--- /dev/null\n+++ b/src/snek/form/register.py\n@@ -0,0 +1,31 @@\n+from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement\n+\n+class RegisterForm(Form):\n+\n+ title = HTMLElement(tag=\"h1\", text=\"Register\")\n+\n+ username = FormInputElement(\n+ name=\"username\", \n+ required=True,\n+ min_length=2,\n+ max_length=20,\n+ regex=r\"^[a-zA-Z0-9_]+$\",\n+ place_holder=\"Username\",\n+ type=\"text\"\n+ )\n+ email = FormInputElement(\n+ name=\"email\",\n+ required=True,\n+ regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n+ place_holder=\"Email address\",\n+ type=\"email\"\n+ )\n+ password = FormInputElement(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\",type=\"password\",place_holder=\"Password\")\n+\n+ action = FormButtonElement(\n+ name=\"action\",\n+ value=\"submit\",\n+ text=\"Register\",\n+ type=\"button\"\n+ )\n+\ndiff --git a/src/snek/forms.py b/src/snek/forms.py\ndeleted file mode 100644\nindex f4a7b24..0000000\n--- a/src/snek/forms.py\n+++ /dev/null\n@@ -1,40 +0,0 @@\n-from snek import models \n-\n-class FormElement(models.ModelField):\n-\n- def __init__(self,html_type, place_holder=None, *args, **kwargs):\n- super().__init__(*args, **kwargs)\n- self.place_holder = place_holder\n- self.html_type = html_type \n-\n- def to_json(self):\n- data = super().to_json()\n- data[\"html_type\"] = self.html_type\n- data[\"place_holder\"] = self.place_holder\n- return data \n-\n-class Form(models.BaseModel):\n- pass\n-\n-class RegisterForm(Form):\n-\n- username = FormElement(\n- name=\"username\", \n- required=True,\n- min_length=2,\n- max_length=20,\n- regex=r\"^[a-zA-Z0-9_]+$\",\n- place_holder=\"Username\",\n- html_type=\"text\"\n- )\n- email = FormElement(\n- name=\"email\",\n- required=True,\n- regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n- place_holder=\"Email address\",\n- html_type=\"email\"\n- )\n- password = FormElement(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",html_type=\"password\")\n-\n-\n-\ndiff --git a/src/snek/gunicorn.py b/src/snek/gunicorn.py\nindex 4055bbd..8583142 100644\n--- a/src/snek/gunicorn.py\n+++ b/src/snek/gunicorn.py\n@@ -1,3 +1,3 @@\n-from snek.app import app \n+from snek.app import app\n \n application = app\ndiff --git a/src/snek/model/__init__.py b/src/snek/model/__init__.py\nnew file mode 100644\nindex 0000000..e69de29\ndiff --git a/src/snek/model/user.py b/src/snek/model/user.py\nnew file mode 100644\nindex 0000000..44553f8\n--- /dev/null\n+++ b/src/snek/model/user.py\n@@ -0,0 +1,19 @@\n+from snek.system.model import BaseModel,ModelField\n+\n+class User(BaseModel):\n+ \n+ username = ModelField(\n+ name=\"username\", \n+ required=True,\n+ min_length=2,\n+ max_length=20,\n+ regex=r\"^[a-zA-Z0-9_]+$\",\n+ )\n+ email = ModelField(\n+ name=\"email\",\n+ required=True,\n+ regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\"\n+ )\n+ password = ModelField(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\")\n+\n+\ndiff --git a/src/snek/static/app.js b/src/snek/static/app.js\nindex 701beda..f06ee24 100644\n--- a/src/snek/static/app.js\n+++ b/src/snek/static/app.js\n@@ -57,13 +57,26 @@ class Room {\n this.name = name \n }\n setMessages(list){\n- \n+\n }\n \n \n }\n \n \n+class InlineAppElement extends HTMLElement {\n+ \n+ constructor(){\n+ this.\n+ }\n+\n+}\n+\n+class Page {\n+ elements = []\n+\n+}\n+\n class App {\n rooms = []\n constructor() {\ndiff --git a/src/snek/static/styles.css b/src/snek/static/base.css\nsimilarity index 94%\nrename from src/snek/static/styles.css\nrename to src/snek/static/base.css\nindex 83c1fb1..363e4f1 100644\n--- a/src/snek/static/styles.css\n+++ b/src/snek/static/base.css\n@@ -1,11 +1,9 @@\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n body {\n font-family: Arial, sans-serif;\n@@ -16,7 +14,6 @@ body {\n height: 100vh;\n }\n \n header {\n padding: 10px 20px;\n@@ -43,14 +40,12 @@ header nav a:hover {\n }\n \n main {\n display: flex;\n flex: 1;\n overflow: hidden;\n }\n \n .sidebar {\n width: 250px;\n@@ -84,7 +79,6 @@ main {\n }\n \n .chat-area {\n flex: 1;\n display: flex;\n@@ -103,7 +97,6 @@ main {\n }\n \n .chat-messages {\n flex: 1;\n padding: 20px;\n@@ -155,7 +148,6 @@ main {\n }\n \n .chat-input {\n padding: 15px;\n@@ -190,7 +182,6 @@ main {\n }\n \n @media (max-width: 768px) {\n .sidebar {\n display: none;\ndiff --git a/src/snek/static/fancy-button.js b/src/snek/static/fancy-button.js\nnew file mode 100644\nindex 0000000..5407a3b\n--- /dev/null\n+++ b/src/snek/static/fancy-button.js\n@@ -0,0 +1,54 @@\n+\n+\n+class FancyButton extends HTMLElement {\n+ url = null\n+ type=\"button\"\n+ value = null\n+ constructor(){\n+ super()\n+ this.attachShadow({mode:'open'})\n+ this.container = document.createElement('span')\n+ this.styleElement = document.createElement(\"style\")\n+ this.styleElement.innerHTML = `\n+ :root {\n+ width:100%;\n+ --width: 100%;\n+ }\n+ button {\n+ width: var(--width);\n+ min-width: 33%;\n+ padding: 10px;\n+ border: none;\n+ border-radius: 5px;\n+ color: white;\n+ font-size: 1em;\n+ font-weight: bold;\n+ cursor: pointer;\n+ transition: background-color 0.3s;\n+ }\n+ button:hover {\n+ }\n+ `\n+ this.container.appendChild(this.styleElement)\n+ this.buttonElement = document.createElement('button')\n+ this.container.appendChild(this.buttonElement)\n+ this.shadowRoot.appendChild(this.container)\n+ }\n+\n+ connectedCallback() {\n+ this.url = this.getAttribute('url');\n+ this.value = this.getAttribute('value')\n+ const me = this \n+ this.buttonElement.appendChild(document.createTextNode(this.getAttribute(\"text\")))\n+ this.buttonElement.addEventListener(\"click\",()=>{\n+ if(me.url){\n+ window.location = me.url\n+ }\n+ })\n+ }\n+}\n+\n+customElements.define(\"fancy-button\",FancyButton)\ndiff --git a/src/snek/static/generic-form.css b/src/snek/static/generic-form.css\nnew file mode 100644\nindex 0000000..d593816\n--- /dev/null\n+++ b/src/snek/static/generic-form.css\n@@ -0,0 +1,100 @@\n+* {\n+ margin: 0;\n+ padding: 0;\n+ box-sizing: border-box;\n+ }\n+ \n+ body {\n+ font-family: Arial, sans-serif;\n+ line-height: 1.5;\n+ display: flex;\n+ flex-direction: column;\n+ justify-content: center;\n+ align-items: center;\n+ height: 100vh;\n+ }\n+\n+generic-form {\n+ margin: 0;\n+ padding: 0;\n+ box-sizing: border-box;\n+\n+}\n+\n+.generic-form-container {\n+ \n+ border-radius: 10px;\n+ padding: 30px;\n+ width: 400px;\n+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n+ text-align: center;\n+\n+}\n+\n+.generic-form-container h1 {\n+ font-size: 2em;\n+ margin-bottom: 20px;\n+}\n+input {\n+\n+}\n+.generic-form-container generic-field {\n+ width: 100%;\n+ padding: 10px;\n+ margin: 10px 0;\n+ border-radius: 5px;\n+ font-size: 1em;\n+}\n+\n+.generic-form-container button {\n+ width: 100%;\n+ padding: 10px;\n+ border: none;\n+ border-radius: 5px;\n+ color: white;\n+ font-size: 1em;\n+ font-weight: bold;\n+ cursor: pointer;\n+ transition: background-color 0.3s;\n+}\n+\n+.generic-form-container button:hover {\n+}\n+\n+.generic-form-container a {\n+ text-decoration: none;\n+ display: block;\n+ margin-top: 15px;\n+ font-size: 0.9em;\n+ transition: color 0.3s;\n+}\n+\n+.generic-form-container a:hover {\n+}\n+\n+\n+.error {\n+ font-size: 0.9em;\n+ margin-top: 5px;\n+}\n+\n+\n+@media (max-width: 500px) {\n+ .generic-form-container {\n+ width: 90%;\n+ }\n+}\n\\ No newline at end of file\ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nnew file mode 100644\nindex 0000000..11fea47\n--- /dev/null\n+++ b/src/snek/static/generic-form.js\n@@ -0,0 +1,321 @@\n+\n+class GenericField extends HTMLElement {\n+ form = null\n+ field = null \n+ inputElement = null\n+ footerElement = null \n+ action = null \n+ container = null\n+ styleElement = null\n+ name = null\n+ get value() {\n+ return this.inputElement.value\n+ }\n+ get type() {\n+\n+ return this.field.tag \n+ }\n+ set value(val) {\n+ val = val == null ? '' : val \n+ this.inputElement.value = val \n+ this.inputElement.setAttribute(\"value\", val)\n+ }\n+ setInvalid(){\n+ this.inputElement.classList.add(\"error\")\n+ this.inputElement.classList.remove(\"valid\")\n+ }\n+ setErrors(errors){\n+ if(errors.length)\n+ this.inputElement.setAttribute(\"title\", errors[0])\n+ else\n+ this.inputElement.setAttribute(\"title\",\"\")\n+ }\n+ setValid(){\n+ this.inputElement.classList.remove(\"error\")\n+ this.inputElement.classList.add(\"valid\")\n+ }\n+ constructor() {\n+ super()\n+ this.attachShadow({mode:'open'})\n+ this.container = document.createElement('div')\n+ this.styleElement = document.createElement('style')\n+ this.styleElement.innerHTML = `\n+\n+ h1 {\n+ font-size: 2em;\n+ margin-bottom: 20px;\n+ margin-top: 0px;\n+ }\n+\n+ input {\n+ width: 90%;\n+ padding: 10px;\n+ margin: 10px 0;\n+ border-radius: 5px;\n+ font-size: 1em;\n+ }\n+\n+ button {\n+ width: 50%;\n+ padding: 10px;\n+ border: none;\n+ float: right;\n+ margin-top: 10px;\n+ margin-left: 10px;\n+ margin-right: 10px;\n+ border-radius: 5px;\n+ color: white;\n+ font-size: 1em;\n+ font-weight: bold;\n+ cursor: pointer;\n+ transition: background-color 0.3s;\n+ clear: both;\n+ }\n+\n+ button:hover {\n+ }\n+\n+ a {\n+ text-decoration: none;\n+ display: block;\n+ margin-top: 15px;\n+ font-size: 0.9em;\n+ transition: color 0.3s;\n+ }\n+\n+ a:hover {\n+ }\n+ .valid {\n+ border: 1px solid green;\n+ color:green;\n+ font-size: 0.9em;\n+ margin-top: 5px;\n+ }\n+ .error {\n+ border: 3px solid red;\n+ font-size: 0.9em;\n+ margin-top: 5px;\n+ }\n+ @media (max-width: 500px) {\n+ input {\n+ width: 90%;\n+ }\n+ } \n+ \n+ `\n+ this.container.appendChild(this.styleElement)\n+ \n+ this.shadowRoot.appendChild(this.container)\n+ }\n+ connectedCallback(){\n+\n+ this.updateAttributes() \n+ \n+ }\n+ setAttribute(name,value){\n+ this[name] = value \n+ }\n+ updateAttributes(){\n+ if(this.inputElement == null && this.field){\n+ this.inputElement = document.createElement(this.field.tag)\n+ if(this.field.tag == 'button'){\n+ if(this.field.value == \"submit\"){\n+ \n+ \n+ }\n+ this.action = this.field.value\n+ }\n+ this.inputElement.name = this.field.name \n+ this.name = this.inputElement.name\n+ const me = this\n+ this.inputElement.addEventListener(\"keyup\",(e)=>{\n+ if(e.key == 'Enter'){\n+ me.dispatchEvent(new Event(\"submit\"))\n+ }else if(me.field.value != e.target.value)\n+ {\n+ const event = new CustomEvent(\"change\", {detail:me,bubbles:true})\n+ me.dispatchEvent(event)\n+ }\n+ })\n+ this.inputElement.addEventListener(\"click\",(e)=>{\n+ const event = new CustomEvent(\"click\",{detail:me,bubbles:true})\n+ me.dispatchEvent(event) \n+ })\n+ this.container.appendChild(this.inputElement)\n+\n+}\n+ if(!this.field){\n+ return\n+ }\n+ this.inputElement.setAttribute(\"type\",this.field.type == null ? 'input' : this.field.type)\n+ this.inputElement.setAttribute(\"name\",this.field.name == null ? '' : this.field.name)\n+ \n+ if(this.field.text != null){\n+ this.inputElement.innerText = this.field.text \n+ }\n+ if(this.field.html != null){\n+ this.inputElement.innerHTML = this.field.html\n+ }\n+ if(this.field.class_name){\n+ this.inputElement.classList.add(this.field.class_name)\n+ }\n+ this.inputElement.setAttribute(\"tabindex\", this.field.index)\n+ this.inputElement.classList.add(this.field.name)\n+ this.value = this.field.value\n+ let place_holder = null \n+ if(this.field.place_holder)\n+ place_holder = this.field.place_holder\n+ if(this.field.required && place_holder){\n+ place_holder = place_holder\n+ }\n+ if(place_holder)\n+ this.field.place_holder = \"* \" + place_holder\n+ this.inputElement.setAttribute(\"placeholder\",place_holder)\n+ if(this.field.required)\n+ this.inputElement.setAttribute(\"required\",\"required\")\n+ else\n+ this.inputElement.removeAttribute(\"required\")\n+ if(!this.footerElement){\n+ this.footerElement = document.createElement('div')\n+ this.footerElement.style.clear = 'both'\n+ this.container.appendChild(this.footerElement)\n+ }\n+ }\n+}\n+\n+customElements.define('generic-field', GenericField);\n+\n+class GenericForm extends HTMLElement {\n+ fields = {} \n+ form = {}\n+ constructor() {\n+\n+\n+ super();\n+ this.attachShadow({ mode: 'open' });\n+ this.styleElement = document.createElement(\"style\")\n+ this.styleElement.innerHTML = `\n+\n+ * {\n+ margin: 0;\n+ padding: 0;\n+ box-sizing: border-box;\n+ width:90%\n+\n+ }\n+\n+ div {\n+ \n+ border-radius: 10px;\n+ padding: 30px;\n+ width: 400px;\n+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n+ text-align: center;\n+\n+ }\n+ @media (max-width: 500px) {\n+ form {\n+ width: 80%;\n+ }\n+ }`\n+ \n+ this.container = document.createElement('div');\n+ this.container.appendChild(this.styleElement)\n+ this.container.classList.add(\"generic-form-container\")\n+ this.shadowRoot.appendChild(this.container);\n+ }\n+\n+ connectedCallback() {\n+ const url = this.getAttribute('url');\n+ if (url) {\n+ const fullUrl = url.startsWith(\"/\") ? window.location.origin + url : new URL(window.location.origin + \"/http-get\")\n+ if(!url.startsWith(\"/\"))\n+ fullUrl.searchParams.set('url', url) \n+ this.loadForm(fullUrl.toString());\n+ } else {\n+ this.container.textContent = \"No URL provided!\";\n+ }\n+ }\n+\n+ async loadForm(url) {\n+ const me = this \n+ try {\n+ const response = await fetch(url);\n+ if (!response.ok) {\n+ throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);\n+ }\n+ me.form = await response.json();\n+ \n+ let fields = Object.values(me.form.fields)\n+ \n+ fields = fields.sort((a,b)=>{\n+ console.info(a.index,b.index)\n+ return a.index - b.index\n+ }) \n+ fields.forEach(field=>{\n+ const fieldElement = document.createElement('generic-field')\n+ me.fields[field.name] = fieldElement \n+ fieldElement.setAttribute(\"form\", me)\n+ fieldElement.setAttribute(\"field\", field)\n+ me.container.appendChild(fieldElement)\n+ fieldElement.updateAttributes() \n+ fieldElement.addEventListener(\"change\",(e)=>{\n+ me.form.fields[e.detail.name].value = e.detail.value\n+ })\n+ fieldElement.addEventListener(\"click\",async (e)=>{\n+ if(e.detail.type == \"button\"){\n+ if(e.detail.value == \"submit\")\n+ {\n+ await me.validate()\n+ }\n+ }\n+ \n+ })\n+ })\n+ \n+ } catch (error) {\n+ this.container.textContent = `Error: ${error.message}`;\n+ }\n+ }\n+ async validate(){\n+ const url = this.getAttribute(\"url\")\n+ const me = this\n+ const response = await fetch(url,{\n+ method: 'POST',\n+ headers: {\n+ 'Content-Type': 'application/json'\n+ },\n+ body: JSON.stringify({\"action\":\"validate\", \"form\":me.form})\n+ });\n+ const form = await response.json()\n+ Object.values(form.fields).forEach(field=>{\n+ if(!me.form.fields[field.name])\n+ return\n+ me.form.fields[field.name].is_valid = field.is_valid \n+ if(!field.is_valid){\n+ me.fields[field.name].setInvalid()\n+ me.fields[field.name].setErrors(field.errors)\n+ console.info(field.name,\"is invalid\")\n+ }else{\n+ me.fields[field.name].setValid()\n+ }\n+ me.fields[field.name].setAttribute(\"field\",field)\n+ me.fields[field.name].updateAttributes()\n+ })\n+ Object.values(form.fields).forEach(field=>{\n+ console.info(field.errors)\n+ me.fields[field.name].setErrors(field.errors)\n+ })\n+ }\n+ }\n+ customElements.define('generic-form', GenericForm);\n\\ No newline at end of file\ndiff --git a/src/snek/static/html_frame.css b/src/snek/static/html-frame.css\nsimilarity index 53%\nrename from src/snek/static/html_frame.css\nrename to src/snek/static/html-frame.css\nindex 6b64c76..92a1f97 100644\n--- a/src/snek/static/html_frame.css\n+++ b/src/snek/static/html-frame.css\n@@ -1,9 +1,6 @@\n .html-frame {\n width: 100px;\n height: 50px;\n- position: relative;\n overflow: hidden;\n border: 1px solid black;\n-\n }\n\\ No newline at end of file\ndiff --git a/src/snek/static/html_frame.js b/src/snek/static/html-frame.js\nsimilarity index 62%\nrename from src/snek/static/html_frame.js\nrename to src/snek/static/html-frame.js\nindex 19a0c34..22581ce 100644\n--- a/src/snek/static/html_frame.js\n+++ b/src/snek/static/html-frame.js\n@@ -12,28 +12,25 @@ class HTMLFrame extends HTMLElement {\n if (url) {\n const fullUrl = url.startsWith(\"/\") ? window.location.origin + url : new URL(window.location.origin + \"/http-get\")\n if(!url.startsWith(\"/\"))\n- fullUrl.searchParams.set('url', url)\n- console.info(fullUrl) \n- this.fetchAndDisplayHtml(fullUrl.toString());\n+ fullUrl.searchParams.set('url', url) \n+ this.loadAndRender(fullUrl.toString());\n } else {\n- this.container.textContent = \"No URL provided!\";\n+ this.container.textContent = \"No source URL!\";\n }\n }\n \n- async fetchAndDisplayHtml(url) {\n+ async loadAndRender(url) {\n try {\n const response = await fetch(url);\n if (!response.ok) {\n- throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);\n+ throw new Error(`Error: ${response.status} ${response.statusText}`);\n }\n const html = await response.text();\n+ this.container.innerHTML = html;\n \n } catch (error) {\n this.container.textContent = `Error: ${error.message}`;\n }\n }\n }\n-\n customElements.define('html-frame', HTMLFrame);\n\\ No newline at end of file\ndiff --git a/src/snek/static/register.css b/src/snek/static/register__.css\nsimilarity index 74%\nrename from src/snek/static/register.css\nrename to src/snek/static/register__.css\nindex fc4ca0f..57186b8 100644\n--- a/src/snek/static/register.css\n+++ b/src/snek/static/register__.css\n@@ -1,24 +1,12 @@\n+\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n- body {\n- font-family: Arial, sans-serif;\n- line-height: 1.5;\n- display: flex;\n- flex-direction: column;\n- justify-content: center;\n- align-items: center;\n- height: 100vh;\n- }\n \n+ \n .registration-container {\n border-radius: 10px;\n@@ -26,16 +14,15 @@\n width: 400px;\n box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n text-align: center;\n+ left: calc(50%-200);\n }\n \n .registration-container h1 {\n font-size: 2em;\n margin-bottom: 20px;\n }\n \n .registration-container input {\n width: 100%;\n padding: 10px;\n@@ -47,7 +34,6 @@\n font-size: 1em;\n }\n \n .registration-container button {\n width: 100%;\n padding: 10px;\n@@ -65,7 +51,6 @@\n }\n \n .registration-container a {\n text-decoration: none;\n@@ -79,14 +64,11 @@\n }\n \n .error {\n font-size: 0.9em;\n margin-top: 5px;\n }\n- \n @media (max-width: 500px) {\n .registration-container {\n width: 90%;\ndiff --git a/src/snek/system/__init__.py b/src/snek/system/__init__.py\nnew file mode 100644\nindex 0000000..e69de29\ndiff --git a/src/snek/system/form.py b/src/snek/system/form.py\nnew file mode 100644\nindex 0000000..68f7c0f\n--- /dev/null\n+++ b/src/snek/system/form.py\n@@ -0,0 +1,171 @@\n+from snek.system import model \n+\n+class HTMLElement(model.ModelField):\n+ def __init__(self,id:str=None, tag:str=\"div\", name:str=None,html:str=None, class_name:str=None, text:str=None, *args, **kwargs):\n+ \"\"\"\n+ Create a new HTMLElement.\n+ \n+ :param id: The id of the element\n+ :param tag: The tag of the element\n+ :param name: The name of the element, used to generate a class name if not provided\n+ :param html: The inner html of the element\n+ :param class_name: The class name of the element\n+ :param text: The text of the element\n+ \"\"\"\n+ self.tag = tag\n+ self.text = text\n+ self.id = id \n+ self.class_name = class_name or name\n+ self.html = html \n+ super().__init__(name=name,*args, **kwargs)\n+\n+ def to_json(self):\n+ \"\"\"\n+ Return a json representation of the element.\n+ \n+ This will return a dict with the following keys:\n+ \n+ - text: The text of the element\n+ - id: The id of the element\n+ - html: The inner html of the element\n+ - class_name: The class name of the element\n+ - tag: The tag of the element\n+ \n+ :return: A json representation of the element\n+ :rtype: dict\n+ \"\"\"\n+ result = super().to_json()\n+ result['text'] = self.text \n+ result['id'] = self.id \n+ result['html'] = self.html \n+ result['class_name'] = self.class_name\n+ result['tag'] = self.tag\n+ return result \n+\n+class FormElement(HTMLElement):\n+ pass\n+ \n+class FormInputElement(FormElement):\n+\n+ def __init__(self,type=\"text\",place_holder=None, *args, **kwargs):\n+ \"\"\"\n+ Initialize a FormInputElement with specified attributes.\n+\n+ :param type: The type of the input element (default is \"text\").\n+ :param place_holder: The placeholder text for the input element.\n+ :param args: Additional positional arguments.\n+ :param kwargs: Additional keyword arguments.\n+ \"\"\"\n+\n+ super().__init__(tag=\"input\", *args, **kwargs)\n+ self.place_holder = place_holder \n+ self.type = type\n+ \n+\n+ def to_json(self):\n+ \"\"\"\n+ Return a json representation of the element.\n+\n+ This will return a dict with the following keys:\n+\n+ - place_holder: The placeholder text for the input element\n+ - type: The type of the input element\n+\n+ :return: A json representation of the element\n+ :rtype: dict\n+ \"\"\"\n+ data = super().to_json()\n+ data[\"place_holder\"] = self.place_holder\n+ data[\"type\"] = self.type\n+ return data \n+ \n+class FormButtonElement(FormElement):\n+ def __init__(self, tag=\"button\", *args, **kwargs):\n+ \"\"\"\n+ Initialize a FormButtonElement with specified attributes.\n+\n+ :param tag: The tag of the button element (default is \"button\").\n+ :param args: Additional positional arguments.\n+ :param kwargs: Additional keyword arguments.\n+ \"\"\"\n+ super().__init__(tag=tag, *args, **kwargs)\n+\n+\n+class Form(model.BaseModel):\n+ \n+ @property\n+ def html_elements(self):\n+ \"\"\"\n+ Return a list of all :class:`HTMLElement` objects in the form.\n+\n+ This is a convenience property that filters the :attr:`fields` list to only\n+ include elements that are instances of :class:`HTMLElement`.\n+\n+ :return: A list of :class:`HTMLElement` objects\n+ :rtype: list\n+ \"\"\"\n+ json_elements = super().to_json()\n+ return [element for element in self.fields if isinstance(element,HTMLElement)]\n+ def set_user_data(self, data):\n+ \"\"\"\n+ Set user data for the form by updating the fields with the provided data.\n+\n+ This method extracts the 'fields' key from the provided data dictionary\n+ and passes it to the parent class's `set_user_data` method to update the\n+ form fields accordingly.\n+\n+ :param data: A dictionary containing the form data, expected to have a \n+ 'fields' key with the data to update the form fields.\n+ \"\"\"\n+\n+ return super().set_user_data(data.get('fields'))\n+\n+ def to_json(self, encode=False):\n+ \"\"\"\n+ Return a JSON representation of the form, including field values and metadata.\n+\n+ This method returns a dictionary with the following keys:\n+\n+ - ``fields``: A dictionary of field names to their current values.\n+ - ``is_valid``: A boolean indicating whether the form is valid.\n+ - ``errors``: A dictionary of field names to lists of error strings.\n+\n+ If the ``encode`` argument is ``True``, the dictionary will be JSON-encoded\n+ before being returned. Otherwise, the dictionary is returned directly.\n+\n+ :param encode: If ``True``, JSON-encode the returned dictionary.\n+ :type encode: bool\n+ :return: A JSON representation of the form.\n+ :rtype: dict\n+ \"\"\"\n+ elements = super().to_json()\n+ html_elements = {}\n+ for element in elements.keys():\n+ print(\"DDD!\",element,flush=True)\n+ field = getattr(self,element)\n+ if isinstance(field,HTMLElement):\n+ print(\"QQQQ!\",element,flush=True)\n+ try:\n+ html_elements[element] = elements[element]\n+ except KeyError:\n+ pass \n+\n+ return dict(fields=html_elements,is_valid=self.is_valid,errors=self.errors)\n+ @property\n+ def errors(self):\n+ \"\"\"\n+ Return a list of all error strings from all fields in the form.\n+\n+ The list will be empty if all fields are valid.\n+\n+ :return: A list of error strings.\n+ :rtype: list\n+ \"\"\"\n+ result = []\n+ for field in self.html_elements:\n+ result += field.errors \n+ return result \n+ @property\n+ def is_valid(self):\n+ return all(element.is_valid for element in self.html_elements)\ndiff --git a/src/snek/http.py b/src/snek/system/http.py\nsimilarity index 100%\nrename from src/snek/http.py\nrename to src/snek/system/http.py\ndiff --git a/src/snek/middleware.py b/src/snek/system/middleware.py\nsimilarity index 100%\nrename from src/snek/middleware.py\nrename to src/snek/system/middleware.py\ndiff --git a/src/snek/models.py b/src/snek/system/model.py\nsimilarity index 78%\nrename from src/snek/models.py\nrename to src/snek/system/model.py\nindex ec5d9ce..a699a9e 100644\n--- a/src/snek/models.py\n+++ b/src/snek/system/model.py\n@@ -23,7 +23,7 @@ def validate_attrs(required=False,min_length=None,max_length=None,regex=None,**k\n return add_attrs(required=required,min_length=min_length,max_length=max_length,regex=regex,**kwargs)(func)\n \n class Validator:\n-\n+ _index = 0\n @property\n def value(self):\n return self._value \n@@ -34,12 +34,14 @@ class Validator:\n \n @property\n def initial_value(self):\n- return None\n+ return self.value\n \n def custom_validation(self):\n return True\n \n def __init__(self,required=False,min_num=None,max_num=None,min_length=None,max_length=None,regex=None,value=None,kind=None,help_text=None,**kwargs):\n+ self.index = Validator._index\n+ Validator._index += 1\n self.required = required \n self.min_num = min_num \n self.max_num = max_num\n@@ -47,8 +49,10 @@ class Validator:\n self.max_length = max_length \n self.regex = regex \n self._value = None \n- self.value = value \n- self.type = kind\n+ self.value = value\n+ print(\"xxxx\", value,flush=True) \n+ \n+ self.kind = kind\n self.help_text = help_text \n self.__dict__.update(kwargs)\n @property \n@@ -61,7 +65,7 @@ class Validator:\n if self.value is None:\n return error_list \n \n- if self.type == float or self.type == int:\n+ if self.kind == float or self.kind == int:\n if self.min_num is not None and self.value < self.min_num:\n error_list.append(\"Field should be minimal {}.\".format(self.min_num))\n if self.max_num is not None and self.value > self.max_num:\n@@ -70,10 +74,11 @@ class Validator:\n error_list.append(\"Field should be minimal {} characters long.\".format(self.min_length))\n if self.max_length is not None and len(self.value) > self.max_length:\n error_list.append(\"Field should be maximal {} characters long.\".format(self.max_length))\n+ print(self.regex, self.value,flush=True)\n if not self.regex is None and not self.value is None and not re.match(self.regex, self.value):\n error_list.append(\"Invalid value.\".format(self.regex))\n- if not self.type is None and type(self.value) != self.type:\n- error_list.append(\"Invalid type. It is supposed to be {}.\".format(self.type))\n+ if not self.kind is None and type(self.value) != self.kind:\n+ error_list.append(\"Invalid kind. It is supposed to be {}.\".format(self.kind))\n return error_list \n \n def validate(self):\n@@ -89,6 +94,8 @@ class Validator:\n except ValueError:\n return False\n \n+ \n+\n def to_json(self):\n return {\n \"required\": self.required,\n@@ -98,19 +105,26 @@ class Validator:\n \"max_length\": self.max_length,\n \"regex\": self.regex,\n \"value\": self.value,\n- \"type\": self.type,\n+ \"kind\": str(self.kind),\n \"help_text\": self.help_text,\n \"errors\": self.errors,\n- \"is_valid\": self.is_valid\n+ \"is_valid\": self.is_valid,\n+ \"index\":self.index\n }\n \n class ModelField(Validator):\n+\n+ index = 1\n def __init__(self,name=None,save=True, *args, **kwargs):\n self.name = name \n- \n self.save = save\n super().__init__(*args, **kwargs)\n \n+ def to_json(self):\n+ result = super().to_json()\n+ result['name'] = self.name\n+ return result \n+\n \n class CreatedField(ModelField):\n \n@@ -146,9 +160,11 @@ class BaseModel:\n updated_at = UpdatedField(name=\"updated_at\",regex=TIMESTAMP_REGEX,place_holder=\"Updated at\")\n deleted_at = DeletedField(name=\"deleted_at\",regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\")\n \n+ \n def __init__(self, *args, **kwargs):\n print(self.__dict__)\n print(dir(self.__class__))\n+ self.fields = {}\n for key in dir(self.__class__):\n obj = getattr(self.__class__,key)\n \n@@ -156,6 +172,7 @@ class BaseModel:\n self.__dict__[key] = copy.deepcopy(obj)\n print(\"JAAA\")\n self.__dict__[key].value = kwargs.pop(key,self.__dict__[key].initial_value)\n+ self.fields[key] = self.__dict__[key]\n \n def __setitem__(self, key, value):\n obj = self.__dict__.get(key)\n@@ -169,6 +186,22 @@ class BaseModel:\n return obj.value \n return obj\n \n+ def set_user_data(self, data):\n+ for key, value in data.items():\n+ field = self.fields.get(key)\n+ if not field:\n+ continue \n+ if value.get('name'):\n+ value = value.get('value')\n+ field.value = value\n+ \n+\n+ @property \n+ def is_valid(self):\n+ for field in self.fields.values():\n+ if not field.is_valid:\n+ return False\n+ return True\n \n def __getitem__(self, key):\n obj = self.__dict__.get(key)\n@@ -180,7 +213,7 @@ class BaseModel:\n if isinstance(obj,Validator):\n obj.value = value\n else:\n- setattr(self,key,value)\n@@ -201,6 +234,7 @@ class BaseModel:\n \"updated_at\": self.updated_at.value,\n \"deleted_at\": self.deleted_at.value\n })\n+ \n for key,value in self.__dict__.items(): \n if key == \"record\":\n continue\n@@ -225,39 +259,8 @@ class FormElement(ModelField):\n self.place_holder = place_holder \n super().__init__(*args, **kwargs)\n \n-\n def to_json(self):\n data = super().to_json()\n data[\"name\"] = self.name \n data[\"place_holder\"] = self.place_holder\n return data \n-\n-\n-\n-\n-class TestModel(BaseModel):\n-\n- first_name = FormElement(name=\"first_name\",required=True,min_length=3,max_length=20,regex=r\"^[a-zA-Z0-9_]+$\",place_holder=\"First name\")\n- last_name = FormElement(name=\"last_name\",required=True,min_length=3,max_length=20,regex=r\"^[a-zA-Z0-9_]+$\",place_holder=\"Last name\")\n- email = FormElement(name=\"email\",required=True,regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",place_holder=\"Email address\") \n-\n-class Form:\n- username = FormElement(required=True,min_length=3,max_length=20,regex=r\"^[a-zA-Z0-9_]+$\",place_holder=\"Username\")\n- email = FormElement(required=True,regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",place_holder=\"Email address\")\n- def __init__(self, *args, **kwargs):\n- self.place_holder = kwargs.pop(\"place_holder\",None) \n-\n-\n-if __name__ == \"__main__\":\n- model = TestModel(first_name=\"John\",last_name=\"Doe\",email=\"n9K9p@example.com\",password=\"Password123\")\n- model2 = TestModel(first_name=\"John\",last_name=\"Doe\",email=\"ddd\",password=\"zzz\")\n- model.first_name = \"AAA\"\n- print(model.first_name)\n- print(model.first_name.value)\n- \n- print(model.first_name)\n- print(model.first_name.value)\n- print(model.to_json(True))\n- print(model2.to_json(True))\n- print(model2.record)\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nnew file mode 100644\nindex 0000000..5b1bdf2\n--- /dev/null\n+++ b/src/snek/templates/base.html\n@@ -0,0 +1,31 @@\n+\n+\n+\n+ \n+ \n+ {% block title %}{% endblock %}\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+\n+\n+
\n+ {% block header %}\n+ {% endblock %}\n+\n+
\n+
\n+ \n+ {% block main %}\n+ {% endblock %}\n+
\n+\n+\n\\ No newline at end of file\ndiff --git a/src/snek/templates/base_chat.html b/src/snek/templates/base_chat.html\nnew file mode 100644\nindex 0000000..09c025b\n--- /dev/null\n+++ b/src/snek/templates/base_chat.html\n@@ -0,0 +1,31 @@\n+\n+\n+\n+ \n+ \n+ {% block title %}{% endblock %}\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+\n+\n+
\n+ {% block header %}\n+ {% endblock %}\n+\n+
\n+
\n+ \n+ {% block main %}\n+ {% endblock %}\n+
\n+\n+\n\\ No newline at end of file\ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nnew file mode 100644\nindex 0000000..1ee1f77\n--- /dev/null\n+++ b/src/snek/templates/index.html\n@@ -0,0 +1,20 @@\n+\n+\n+\n+ \n+ \n+ Snek chat by Molodetz\n+ \n+ \n+ \n+\n+\n+
\n+

Snek

\n+ \n+ Or\n+ \n+\n+
\n+\n+\ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex d37f3fa..c09ec70 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -1,20 +1,5 @@\n-\n-\n-\n- \n- \n- Register\n- \n-\n-\n-
\n-

Login

\n-
\n- \n- \n- \n- Not having an account yet? Register here.\n-
\n-
\n-\n-\n+{% extends \"base.html\" %}\n+\n+{% block main %}\n+ \n+{% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex da41629..61da961 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -1,22 +1,5 @@\n-\n-\n-\n- \n- \n- Register\n- \n-\n-\n-
\n-

Register

\n-
\n- \n- \n- \n- \n- \n-
\n-
\n-\n-\n+{% extends \"base.html\" %}\n+\n+{% block main %}\n+ \n+{% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/templates/test.html b/src/snek/templates/web.html\nsimilarity index 92%\nrename from src/snek/templates/test.html\nrename to src/snek/templates/web.html\nindex c23103a..0403e1b 100644\n--- a/src/snek/templates/test.html\n+++ b/src/snek/templates/web.html\n@@ -4,9 +4,7 @@\n \n \n Dark Themed Chat Application\n- \n- \n- \n+ \n \n \n
\n@@ -59,5 +57,4 @@\n \n \n \n-\n-\n+\n\\ No newline at end of file\ndiff --git a/src/snek/view/base.py b/src/snek/view/base.py\nnew file mode 100644\nindex 0000000..d962ee5\n--- /dev/null\n+++ b/src/snek/view/base.py\n@@ -0,0 +1,31 @@\n+from aiohttp import web \n+\n+class BaseView(web.View):\n+ \n+ @property \n+ def app(self):\n+ return self.request.app\n+ \n+ @property\n+ def db(self):\n+ return self.app.db\n+\n+ def json_response(self, data):\n+ return web.json_response(data)\n+\n+ def render_template(self, template_name, context=None):\n+ return self.request.app.render_template(template_name, self.request,context)\n+ \n+class BaseFormView(BaseView):\n+\n+ form = None \n+\n+ async def get(self):\n+ form = self.form()\n+ return self.json_response(form.to_json())\n+ \n+ async def post(self):\n+ form = self.form()\n+ post = await self.request.json()\n+ form.set_user_data(post['form'])\n+ return self.json_response(form.to_json()) \n\\ No newline at end of file\ndiff --git a/src/snek/view/index.py b/src/snek/view/index.py\nnew file mode 100644\nindex 0000000..a5d8b92\n--- /dev/null\n+++ b/src/snek/view/index.py\n@@ -0,0 +1,6 @@\n+from snek.view.base import BaseView\n+\n+class IndexView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"index.html\")\ndiff --git a/src/snek/view/login.py b/src/snek/view/login.py\nnew file mode 100644\nindex 0000000..3a3beaf\n--- /dev/null\n+++ b/src/snek/view/login.py\n@@ -0,0 +1,13 @@\n+from snek.form.register import RegisterForm\n+from snek.view.base import BaseView \n+\n+class LoginView(BaseView):\n+\n+ async def get(self):\n+ \n+ async def post(self):\n+ form = RegisterForm()\n+ form.set_user_data(await self.request.post())\n+ print(form.is_valid())\ndiff --git a/src/snek/view/login_form.py b/src/snek/view/login_form.py\nnew file mode 100644\nindex 0000000..26527da\n--- /dev/null\n+++ b/src/snek/view/login_form.py\n@@ -0,0 +1,5 @@\n+from snek.view.base import BaseFormView\n+from snek.form.login import LoginForm\n+\n+class LoginFormView(BaseFormView):\n+ form = LoginForm\n\\ No newline at end of file\ndiff --git a/src/snek/view/register.py b/src/snek/view/register.py\nnew file mode 100644\nindex 0000000..095b7a3\n--- /dev/null\n+++ b/src/snek/view/register.py\n@@ -0,0 +1,6 @@\n+from snek.view.base import BaseView \n+\n+class RegisterView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"register.html\") \n\\ No newline at end of file\ndiff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py\nnew file mode 100644\nindex 0000000..0ae7630\n--- /dev/null\n+++ b/src/snek/view/register_form.py\n@@ -0,0 +1,5 @@\n+from snek.form.register import RegisterForm\n+from snek.view.base import BaseFormView \n+\n+class RegisterFormView(BaseFormView):\n+ form = RegisterForm\n\\ No newline at end of file\ndiff --git a/src/snek/view/view.py b/src/snek/view/view.py\nnew file mode 100644\nindex 0000000..ea642a3\n--- /dev/null\n+++ b/src/snek/view/view.py\n@@ -0,0 +1,6 @@\n+from snek.view.base import BaseView \n+\n+class WebView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"web.html\")\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor system and views for improved structure and maintainability", "commit": "d20079f3ed8f261bcda0f5379f4c9e23ee941527", "diff": "commit d20079f3ed8f261bcda0f5379f4c9e23ee941527\nAuthor: retoor \nDate: Fri Jan 24 14:00:10 2025 +0100\n\n Complete system.\n\ndiff --git a/.gitignore b/.gitignore\nindex 3747073..ece77be 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -1,5 +1,8 @@\n .vscode\n .history\n+.resources\n+.backup*\n+docs\n *.db*\n *.png\ndiff --git a/Dockerfile b/Dockerfile\nindex 47c0ece..9af8e87 100644\n--- a/Dockerfile\n+++ b/Dockerfile\n@@ -1,5 +1,5 @@\n FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf \n-FROM python:3.10-alpine\n+FROM python:3.12.8-alpine3.21\n WORKDIR /code\n ENV FLASK_APP=app.py\n ENV FLASK_RUN_HOST=0.0.0.0\n@@ -37,5 +37,5 @@ RUN pip install --upgrade pip\n RUN pip install -e .\n EXPOSE 8081\n \n-python -m snek.app\n+CMD [\"python\",\"-m\",\"snek.app\"]\ndiff --git a/pyproject.toml b/pyproject.toml\nindex d98557e..cc36846 100644\n--- a/pyproject.toml\n+++ b/pyproject.toml\n@@ -20,6 +20,8 @@ dependencies = [\n \"beautifulsoup4\",\n \"gunicorn\",\n \"imgkit\",\n- \"wkhtmltopdf\"\n+ \"wkhtmltopdf\",\n+ \"jinja-markdown2\",\n+ \"mistune\"\n ]\n \ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 0e2ed63..deac5d3 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -3,14 +3,16 @@ import pathlib\n from aiohttp import web\n from app.app import Application as BaseApplication\n \n+from jinja_markdown2 import MarkdownExtension\n from snek.system import http\n from snek.system.middleware import cors_middleware\n+from snek.view.about import AboutHTMLView, AboutMDView\n from snek.view.index import IndexView\n from snek.view.login import LoginView\n from snek.view.login_form import LoginFormView\n from snek.view.register import RegisterView\n from snek.view.register_form import RegisterFormView\n-from snek.view.view import WebView\n+from snek.view.web import WebView\n \n \n class Application(BaseApplication):\n@@ -24,6 +26,7 @@ class Application(BaseApplication):\n super().__init__(\n middlewares=middlewares, template_path=self.template_path, *args, **kwargs\n )\n+ self.jinja2_env.add_extension(MarkdownExtension)\n self.setup_router()\n \n def setup_router(self):\n@@ -34,12 +37,14 @@ class Application(BaseApplication):\n name=\"static\",\n show_index=True,\n )\n- self.router.add_view(\"/web\", WebView)\n- self.router.add_view(\"/login\", LoginView)\n- self.router.add_view(\"/login-form\", LoginFormView)\n- self.router.add_view(\"/register\", RegisterView)\n+ self.router.add_view(\"/about.html\", AboutHTMLView)\n+ self.router.add_view(\"/about.md\", AboutMDView)\n+ self.router.add_view(\"/web.html\", WebView)\n+ self.router.add_view(\"/login.html\", LoginView)\n+ self.router.add_view(\"/login-form.json\", LoginFormView)\n+ self.router.add_view(\"/register.html\", RegisterView)\n \n- self.router.add_view(\"/register-form\", RegisterFormView)\n+ self.router.add_view(\"/register-form.json\", RegisterFormView)\n self.router.add_get(\"/http-get\", self.handle_http_get)\n self.router.add_get(\"/http-photo\", self.handle_http_photo)\n \ndiff --git a/src/snek/form/register.py b/src/snek/form/register.py\nindex 60399fb..7dff3e4 100644\n--- a/src/snek/form/register.py\n+++ b/src/snek/form/register.py\n@@ -15,7 +15,7 @@ class RegisterForm(Form):\n )\n email = FormInputElement(\n name=\"email\",\n- required=True,\n+ required=False,\n regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n place_holder=\"Email address\",\n type=\"email\"\ndiff --git a/src/snek/mapper/__init__.py b/src/snek/mapper/__init__.py\nnew file mode 100644\nindex 0000000..dc9e047\n--- /dev/null\n+++ b/src/snek/mapper/__init__.py\n@@ -0,0 +1,12 @@\n+import functools \n+from snek.mapper.user import UserMapper\n+\n+@functools.cache \n+def get_mappers(app=None):\n+ return dict(\n+ user=UserMapper(app=app)\n+\n+ )\n+\n+def get_mapper(name, app=None):\n+ return get_mappers(app=app)[name]\n\\ No newline at end of file\ndiff --git a/src/snek/mapper/user.py b/src/snek/mapper/user.py\nnew file mode 100644\nindex 0000000..5b8671e\n--- /dev/null\n+++ b/src/snek/mapper/user.py\n@@ -0,0 +1,6 @@\n+from snek.system.mapper import BaseMapper\n+from snek.model.user import UserModel\n+\n+class UserMapper(BaseMapper):\n+ table_name = \"user\"\n+ model: UserModel \n\\ No newline at end of file\ndiff --git a/src/snek/model/__init__.py b/src/snek/model/__init__.py\nindex e69de29..52af21a 100644\n--- a/src/snek/model/__init__.py\n+++ b/src/snek/model/__init__.py\n@@ -0,0 +1,12 @@\n+from snek.model.user import UserModel \n+import functools \n+\n+@functools.cache\n+def get_models():\n+ return dict(\n+ user=UserModel\n+\n+ )\n+\n+def get_model(name):\n+ return get_models()[name]\ndiff --git a/src/snek/model/user.py b/src/snek/model/user.py\nindex 44553f8..254b6c9 100644\n--- a/src/snek/model/user.py\n+++ b/src/snek/model/user.py\n@@ -1,6 +1,6 @@\n from snek.system.model import BaseModel,ModelField\n \n-class User(BaseModel):\n+class UserModel(BaseModel):\n \n username = ModelField(\n name=\"username\", \n@@ -11,7 +11,7 @@ class User(BaseModel):\n )\n email = ModelField(\n name=\"email\",\n- required=True,\n+ required=False,\n regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\"\n )\n password = ModelField(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\")\ndiff --git a/src/snek/service/__init__.py b/src/snek/service/__init__.py\nnew file mode 100644\nindex 0000000..4038f70\n--- /dev/null\n+++ b/src/snek/service/__init__.py\n@@ -0,0 +1,12 @@\n+from snek.service.user import UserService \n+import functools \n+\n+@functools.cache\n+def get_services(app):\n+\n+ return dict(\n+ user = UserService(app=app)\n+\n+ )\n+def get_service(name, app=None):\n+ return get_services(app=app)[name]\n\\ No newline at end of file\ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nnew file mode 100644\nindex 0000000..cde4b8c\n--- /dev/null\n+++ b/src/snek/service/user.py\n@@ -0,0 +1,16 @@\n+from snek.system.service import BaseService \n+from snek.system import security \n+\n+class UserService:\n+ mapper_name = \"user\"\n+\n+ async def create_user(self, username, password):\n+ if await self.exists(username=username):\n+ raise Exception(\"User already exists.\")\n+ model = await self.new()\n+ model.username = username\n+ model.password = await security.hash(password)\n+ if await self.save(model):\n+ return model \n+ raise Exception(f\"Failed to create user: {model.errors}.\")\n+ \n\\ No newline at end of file\ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nindex 11fea47..58a67e2 100644\n--- a/src/snek/static/generic-form.js\n+++ b/src/snek/static/generic-form.js\n@@ -224,7 +224,11 @@ class GenericForm extends HTMLElement {\n \n }\n @media (max-width: 500px) {\n+ width:100%;\n+ height:100%;\n form {\n+ height:100%;\n+ width: 100%;\n width: 80%;\n }\n }`\ndiff --git a/src/snek/static/html-frame.js b/src/snek/static/html-frame.js\nindex 22581ce..0d5d4c9 100644\n--- a/src/snek/static/html-frame.js\n+++ b/src/snek/static/html-frame.js\n@@ -26,7 +26,16 @@ class HTMLFrame extends HTMLElement {\n throw new Error(`Error: ${response.status} ${response.statusText}`);\n }\n const html = await response.text();\n- this.container.innerHTML = html;\n+ if(url.endsWith(\".md\")){\n+ const parent = this\n+ const markdownElement = document.createElement('div')\n+ markdownElement.innerHTML = html\n+ document.body.appendChild(markdownElement)\n+ \n+ }else{\n+ this.container.innerHTML = html;\n+ }\n \n } catch (error) {\n this.container.textContent = `Error: ${error.message}`;\ndiff --git a/src/snek/static/markdown-frame.js b/src/snek/static/markdown-frame.js\nnew file mode 100644\nindex 0000000..e2b7a77\n--- /dev/null\n+++ b/src/snek/static/markdown-frame.js\n@@ -0,0 +1,39 @@\n+\n+\n+\n+class HTMLFrame extends HTMLElement {\n+ constructor() {\n+ super();\n+ this.attachShadow({ mode: 'open' });\n+ this.container = document.createElement('div');\n+ this.shadowRoot.appendChild(this.container);\n+ }\n+\n+ connectedCallback() {\n+ this.container.classList.add(\"html_frame\")\n+ const url = this.getAttribute('url');\n+ if (url) {\n+ const fullUrl = url.startsWith(\"/\") ? window.location.origin + url : new URL(window.location.origin + \"/http-get\")\n+ if(!url.startsWith(\"/\"))\n+ fullUrl.searchParams.set('url', url) \n+ this.loadAndRender(fullUrl.toString());\n+ } else {\n+ this.container.textContent = \"No source URL!\";\n+ }\n+ }\n+\n+ async loadAndRender(url) {\n+ try {\n+ const response = await fetch(url);\n+ if (!response.ok) {\n+ throw new Error(`Error: ${response.status} ${response.statusText}`);\n+ }\n+ const html = await response.text();\n+ this.container.innerHTML = html;\n+ \n+ } catch (error) {\n+ this.container.textContent = `Error: ${error.message}`;\n+ }\n+ }\n+ }\n+ customElements.define('markdown-frame', HTMLFrame);\n\\ No newline at end of file\ndiff --git a/src/snek/static/style.css b/src/snek/static/style.css\nnew file mode 100644\nindex 0000000..990fcf9\n--- /dev/null\n+++ b/src/snek/static/style.css\n@@ -0,0 +1,20 @@\n+h1 {\n+ font-size: 2em;\n+ margin-bottom: 20px;\n+}\n+\n+h2 {\n+ font-size: 1.4em;\n+ margin-bottom: 20px;\n+}\n+body {\n+\n+}\n+div {\n+ text-align: left;\n+\n+}\n\\ No newline at end of file\ndiff --git a/src/snek/system/api.py b/src/snek/system/api.py\nnew file mode 100644\nindex 0000000..e69de29\ndiff --git a/src/snek/system/form.py b/src/snek/system/form.py\nindex 68f7c0f..f9ebebb 100644\n--- a/src/snek/system/form.py\n+++ b/src/snek/system/form.py\n@@ -1,171 +1,96 @@\n-from snek.system import model \n+\n+\n+\n+\n+from snek.system import model\n \n class HTMLElement(model.ModelField):\n- def __init__(self,id:str=None, tag:str=\"div\", name:str=None,html:str=None, class_name:str=None, text:str=None, *args, **kwargs):\n- \"\"\"\n- Create a new HTMLElement.\n- \n- :param id: The id of the element\n- :param tag: The tag of the element\n- :param name: The name of the element, used to generate a class name if not provided\n- :param html: The inner html of the element\n- :param class_name: The class name of the element\n- :param text: The text of the element\n- \"\"\"\n+ def __init__(self, id=None, tag=\"div\", name=None, html=None, class_name=None, text=None, *args, **kwargs):\n self.tag = tag\n self.text = text\n- self.id = id \n+ self.id = id\n self.class_name = class_name or name\n- self.html = html \n- super().__init__(name=name,*args, **kwargs)\n+ self.html = html\n+ super().__init__(name=name, *args, **kwargs)\n \n def to_json(self):\n- \"\"\"\n- Return a json representation of the element.\n- \n- This will return a dict with the following keys:\n- \n- - text: The text of the element\n- - id: The id of the element\n- - html: The inner html of the element\n- - class_name: The class name of the element\n- - tag: The tag of the element\n- \n- :return: A json representation of the element\n- :rtype: dict\n- \"\"\"\n result = super().to_json()\n- result['text'] = self.text \n- result['id'] = self.id \n- result['html'] = self.html \n+ result['text'] = self.text\n+ result['id'] = self.id\n+ result['html'] = self.html\n result['class_name'] = self.class_name\n result['tag'] = self.tag\n- return result \n+ return result\n \n class FormElement(HTMLElement):\n pass\n- \n-class FormInputElement(FormElement):\n-\n- def __init__(self,type=\"text\",place_holder=None, *args, **kwargs):\n- \"\"\"\n- Initialize a FormInputElement with specified attributes.\n-\n- :param type: The type of the input element (default is \"text\").\n- :param place_holder: The placeholder text for the input element.\n- :param args: Additional positional arguments.\n- :param kwargs: Additional keyword arguments.\n- \"\"\"\n \n+class FormInputElement(FormElement):\n+ def __init__(self, type=\"text\", place_holder=None, *args, **kwargs):\n super().__init__(tag=\"input\", *args, **kwargs)\n- self.place_holder = place_holder \n+ self.place_holder = place_holder\n self.type = type\n- \n \n def to_json(self):\n- \"\"\"\n- Return a json representation of the element.\n-\n- This will return a dict with the following keys:\n-\n- - place_holder: The placeholder text for the input element\n- - type: The type of the input element\n-\n- :return: A json representation of the element\n- :rtype: dict\n- \"\"\"\n data = super().to_json()\n data[\"place_holder\"] = self.place_holder\n data[\"type\"] = self.type\n- return data \n- \n+ return data\n+\n class FormButtonElement(FormElement):\n def __init__(self, tag=\"button\", *args, **kwargs):\n- \"\"\"\n- Initialize a FormButtonElement with specified attributes.\n-\n- :param tag: The tag of the button element (default is \"button\").\n- :param args: Additional positional arguments.\n- :param kwargs: Additional keyword arguments.\n- \"\"\"\n super().__init__(tag=tag, *args, **kwargs)\n \n-\n class Form(model.BaseModel):\n- \n @property\n def html_elements(self):\n- \"\"\"\n- Return a list of all :class:`HTMLElement` objects in the form.\n-\n- This is a convenience property that filters the :attr:`fields` list to only\n- include elements that are instances of :class:`HTMLElement`.\n-\n- :return: A list of :class:`HTMLElement` objects\n- :rtype: list\n- \"\"\"\n json_elements = super().to_json()\n- return [element for element in self.fields if isinstance(element,HTMLElement)]\n- def set_user_data(self, data):\n- \"\"\"\n- Set user data for the form by updating the fields with the provided data.\n-\n- This method extracts the 'fields' key from the provided data dictionary\n- and passes it to the parent class's `set_user_data` method to update the\n- form fields accordingly.\n-\n- :param data: A dictionary containing the form data, expected to have a \n- 'fields' key with the data to update the form fields.\n- \"\"\"\n+ return [element for element in self.fields if isinstance(element, HTMLElement)]\n \n+ def set_user_data(self, data):\n return super().set_user_data(data.get('fields'))\n \n def to_json(self, encode=False):\n- \"\"\"\n- Return a JSON representation of the form, including field values and metadata.\n-\n- This method returns a dictionary with the following keys:\n-\n- - ``fields``: A dictionary of field names to their current values.\n- - ``is_valid``: A boolean indicating whether the form is valid.\n- - ``errors``: A dictionary of field names to lists of error strings.\n-\n- If the ``encode`` argument is ``True``, the dictionary will be JSON-encoded\n- before being returned. Otherwise, the dictionary is returned directly.\n-\n- :param encode: If ``True``, JSON-encode the returned dictionary.\n- :type encode: bool\n- :return: A JSON representation of the form.\n- :rtype: dict\n- \"\"\"\n elements = super().to_json()\n html_elements = {}\n for element in elements.keys():\n- print(\"DDD!\",element,flush=True)\n- field = getattr(self,element)\n- if isinstance(field,HTMLElement):\n- print(\"QQQQ!\",element,flush=True)\n+ field = getattr(self, element)\n+ if isinstance(field, HTMLElement):\n try:\n html_elements[element] = elements[element]\n except KeyError:\n- pass \n+ pass\n+ return dict(fields=html_elements, is_valid=self.is_valid, errors=self.errors)\n \n- return dict(fields=html_elements,is_valid=self.is_valid,errors=self.errors)\n @property\n def errors(self):\n- \"\"\"\n- Return a list of all error strings from all fields in the form.\n-\n- The list will be empty if all fields are valid.\n-\n- :return: A list of error strings.\n- :rtype: list\n- \"\"\"\n result = []\n for field in self.html_elements:\n- result += field.errors \n- return result \n+ result += field.errors\n+ return result\n+\n @property\n def is_valid(self):\n- return all(element.is_valid for element in self.html_elements)\n+ return all(element.is_valid for element in self.html_elements)\n\\ No newline at end of file\ndiff --git a/src/snek/system/http.py b/src/snek/system/http.py\nindex 0b16bee..b5e8b4f 100644\n--- a/src/snek/system/http.py\n+++ b/src/snek/system/http.py\n@@ -1,77 +1,99 @@\n-from aiohttp import web \n-import aiohttp \n+\n+\n+\n+\n+\n+from aiohttp import web\n+import aiohttp\n from app.cache import time_cache_async\n from bs4 import BeautifulSoup\n from urllib.parse import urljoin\n-import pathlib \n-import uuid \n-import imgkit \n+import pathlib\n+import uuid\n+import imgkit\n import asyncio\n import zlib\n-import io \n+import io\n \n async def crc32(data):\n try:\n data = data.encode()\n except:\n- pass \n- result = \"crc32\" + str(zlib.crc32(data))\n- return result \n+ pass\n+ return \"crc32\" + str(zlib.crc32(data))\n \n-async def get_file(name,suffix=\".cache\"):\n+async def get_file(name, suffix=\".cache\"):\n name = await crc32(name)\n path = pathlib.Path(\".\").joinpath(\"cache\")\n if not path.exists():\n- path.mkdir(parents=True,exist_ok=True)\n- path = path.joinpath(name + suffix)\n- return path\n-\n-\n+ path.mkdir(parents=True, exist_ok=True)\n+ return path.joinpath(name + suffix)\n \n async def public_touch(name=None):\n- path = pathlib.Path(\".\").joinpath(str(uuid.uuid4())+name)\n+ path = pathlib.Path(\".\").joinpath(str(uuid.uuid4()) + name)\n path.open(\"wb\").close()\n- return path \n+ return path\n \n async def create_site_photo(url):\n loop = asyncio.get_event_loop()\n if not url.startswith(\"https\"):\n- output_path = await get_file(\"site-screenshot-\" + url,\".png\")\n+ output_path = await get_file(\"site-screenshot-\" + url, \".png\")\n \n if output_path.exists():\n return output_path\n output_path.touch()\n+\n def make_photo():\n imgkit.from_url(url, output_path.absolute())\n- return output_path \n+ return output_path\n \n- return await loop.run_in_executor(None,make_photo)\n+ return await loop.run_in_executor(None, make_photo)\n \n async def repair_links(base_url, html_content):\n soup = BeautifulSoup(html_content, \"html.parser\")\n for tag in soup.find_all(['a', 'img', 'link']):\n+ if tag.has_attr('href') and not tag['href'].startswith(\"http\"):\n tag['href'] = urljoin(base_url, tag['href'])\n+ if tag.has_attr('src') and not tag['src'].startswith(\"http\"):\n tag['src'] = urljoin(base_url, tag['src'])\n- print(\"Fixed: \",tag['src'])\n return soup.prettify()\n \n async def is_html_content(content: bytes):\n try:\n content = content.decode(errors='ignore')\n except:\n- pass \n- marks = [' types.Optional[BaseModel]\n+ if uid:\n+ kwargs['uid'] = uid \n+ model = self.new()\n+ record = self.table.find_one(**kwargs)\n+ return self.model_class.from_record(mapper=self,record=record)\n+\n+ async def exists(self, **kwargs):\n+ return self.table.exists(**kwargs)\n+\n+ async def count(self, **kwargs) -> int:\n+ return self.table.count(**kwargs)\n+\n+ async def save(self, model:BaseModel) -> bool:\n+ record = model.record\n+ if not record.get('uid'):\n+ raise Exception(f\"Attempt to save without uid: {record}.\")\n+ return self.table.upsert(record,['uid'])\n+\n+ async def find(self, **kwargs) -> types.List[BaseModel]:\n+ if not kwargs.get(\"_limit\"):\n+ kwargs[\"_limit\"] = self.default_limit\n+ for record in self.table.find(**kwargs):\n+ yield self.model_class.from_record(mapper=self,record=record)\n+ \n+ async def delete(self, kwargs=None)-> int: \n+ if not kwargs or not isinstance(kwargs, dict):\n+ raise Exception(\"Can't execute delete with no filter.\")\n+ return self.table.delete(**kwargs)\ndiff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py\nnew file mode 100644\nindex 0000000..bded949\n--- /dev/null\n+++ b/src/snek/system/markdown.py\n@@ -0,0 +1,43 @@\n+\n+\n+from mistune import escape\n+from mistune import Markdown\n+from mistune import HTMLRenderer\n+from pygments import highlight\n+from pygments.lexers import get_lexer_by_name\n+from pygments.formatters import html\n+from pygments.styles import get_style_by_name\n+\n+\n+class MarkdownRenderer(HTMLRenderer):\n+ def __init__(self, app, template):\n+ self.template = template\n+ \n+ self.app = app\n+ self.env = self.app.jinja2_env\n+ formatter = html.HtmlFormatter()\n+ self.env.globals['highlight_styles'] = formatter.get_style_defs()\n+ def _escape(self,str):\n+ def block_code(self, code, lang=None,info=None):\n+ if not lang:\n+ lang = info\n+ if not lang:\n+ return f\"
{code}
\"\n+ lexer = get_lexer_by_name(lang, stripall=True)\n+ formatter = html.HtmlFormatter(lineseparator=\"
\")\n+ print(code, lang,info, flush=True)\n+ return highlight(code, lexer, formatter)\n+ def render(self):\n+ markdown_string = self.app.template_path.joinpath(self.template).read_text()\n+ renderer = MarkdownRenderer(self.app,self.template)\n+ markdown = Markdown(renderer=renderer)\n+ return markdown(markdown_string)\n+\n+\n+async def render_markdown(app, markdown_string):\n+ renderer = MarkdownRenderer(app,None)\n+ markdown = Markdown(renderer=renderer)\n+ return markdown(markdown_string)\n\\ No newline at end of file\ndiff --git a/src/snek/system/middleware.py b/src/snek/system/middleware.py\nindex 6b801ab..7fe457f 100644\n--- a/src/snek/system/middleware.py\n+++ b/src/snek/system/middleware.py\n@@ -1,4 +1,12 @@\n-from aiohttp import web \n+\n+\n+\n+\n+from aiohttp import web\n \n @web.middleware\n async def no_cors_middleware(request, handler):\n@@ -7,16 +15,15 @@ async def no_cors_middleware(request, handler):\n return response\n \n @web.middleware\n-async def cors_allow_middleware(request ,handler):\n+async def cors_allow_middleware(request, handler):\n response = await handler(request)\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n response.headers[\"Access-Control-Allow-Methods\"] = \"GET, POST, OPTIONS, PUT, DELETE\"\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n- return response \n+ return response\n \n @web.middleware\n async def cors_middleware(request, handler):\n if request.method == \"OPTIONS\":\n response = web.Response()\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n@@ -24,7 +31,6 @@ async def cors_middleware(request, handler):\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n return response\n \n response = await handler(request)\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n response.headers[\"Access-Control-Allow-Methods\"] = \"GET, POST, PUT, DELETE, OPTIONS\"\ndiff --git a/src/snek/system/model.py b/src/snek/system/model.py\nindex a699a9e..eb490b3 100644\n--- a/src/snek/system/model.py\n+++ b/src/snek/system/model.py\n@@ -1,36 +1,67 @@\n+\n+\n+\n+\n+\n import re\n import uuid\n-import json \n-from datetime import datetime , timezone \n+import json\n+from datetime import datetime, timezone\n from collections import OrderedDict\n-import copy \n+import copy\n \n TIMESTAMP_REGEX = r\"^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}\\+\\d{2}:\\d{2}$\"\n \n+\n def now():\n return str(datetime.now(timezone.utc))\n \n+\n def add_attrs(**kwargs):\n def decorator(func):\n for key, value in kwargs.items():\n setattr(func, key, value)\n-\n return func\n return decorator\n \n-def validate_attrs(required=False,min_length=None,max_length=None,regex=None,**kwargs):\n- def decorator(func):\n- return add_attrs(required=required,min_length=min_length,max_length=max_length,regex=regex,**kwargs)(func)\n+\n+def validate_attrs(required=False, min_length=None, max_length=None, regex=None, **kwargs):\n+ def decorator(func):\n+ return add_attrs(required=required, min_length=min_length, max_length=max_length, regex=regex, **kwargs)(func)\n+\n \n class Validator:\n _index = 0\n+\n @property\n def value(self):\n- return self._value \n+ return self._value\n \n- @value.setter \n- def value(self,val):\n- self._value = json.loads(json.dumps(val,default=str))\n+ @value.setter\n+ def value(self, val):\n+ self._value = json.loads(json.dumps(val, default=str))\n \n @property\n def initial_value(self):\n@@ -39,48 +70,49 @@ class Validator:\n def custom_validation(self):\n return True\n \n- def __init__(self,required=False,min_num=None,max_num=None,min_length=None,max_length=None,regex=None,value=None,kind=None,help_text=None,**kwargs):\n+ def __init__(self, required=False, min_num=None, max_num=None, min_length=None, max_length=None, regex=None, value=None, kind=None, help_text=None, **kwargs):\n self.index = Validator._index\n Validator._index += 1\n- self.required = required \n- self.min_num = min_num \n+ self.required = required\n+ self.min_num = min_num\n self.max_num = max_num\n- self.min_length = min_length \n- self.max_length = max_length \n- self.regex = regex \n- self._value = None \n+ self.min_length = min_length\n+ self.max_length = max_length\n+ self.regex = regex\n+ self._value = None\n self.value = value\n- print(\"xxxx\", value,flush=True) \n- \n+ print(\"xxxx\", value, flush=True)\n+\n self.kind = kind\n- self.help_text = help_text \n+ self.help_text = help_text\n self.__dict__.update(kwargs)\n- @property \n+\n+ @property\n def errors(self):\n error_list = []\n if self.value is None and self.required:\n error_list.append(\"Field is required.\")\n- return error_list \n- \n+ return error_list\n+\n if self.value is None:\n- return error_list \n+ return error_list\n \n- if self.kind == float or self.kind == int:\n+ if self.kind in [int, float]:\n if self.min_num is not None and self.value < self.min_num:\n- error_list.append(\"Field should be minimal {}.\".format(self.min_num))\n+ error_list.append(f\"Field should be minimal {self.min_num}.\")\n if self.max_num is not None and self.value > self.max_num:\n- error_list.append(\"Field should be maximal {}.\".format(self.max_num))\n+ error_list.append(f\"Field should be maximal {self.max_num}.\")\n if self.min_length is not None and len(self.value) < self.min_length:\n- error_list.append(\"Field should be minimal {} characters long.\".format(self.min_length))\n+ error_list.append(f\"Field should be minimal {self.min_length} characters long.\")\n if self.max_length is not None and len(self.value) > self.max_length:\n- error_list.append(\"Field should be maximal {} characters long.\".format(self.max_length))\n- print(self.regex, self.value,flush=True)\n- if not self.regex is None and not self.value is None and not re.match(self.regex, self.value):\n- error_list.append(\"Invalid value.\".format(self.regex))\n- if not self.kind is None and type(self.value) != self.kind:\n- error_list.append(\"Invalid kind. It is supposed to be {}.\".format(self.kind))\n- return error_list \n- \n+ error_list.append(f\"Field should be maximal {self.max_length} characters long.\")\n+ print(self.regex, self.value, flush=True)\n+ if self.regex and self.value and not re.match(self.regex, self.value):\n+ error_list.append(\"Invalid value.\")\n+ if self.kind and not isinstance(self.value, self.kind):\n+ error_list.append(f\"Invalid kind. It is supposed to be {self.kind}.\")\n+ return error_list\n+\n def validate(self):\n if self.errors:\n raise ValueError(\"\\n\", self.errors)\n@@ -94,8 +126,6 @@ class Validator:\n except ValueError:\n return False\n \n- \n-\n def to_json(self):\n return {\n \"required\": self.required,\n@@ -109,25 +139,27 @@ class Validator:\n \"help_text\": self.help_text,\n \"errors\": self.errors,\n \"is_valid\": self.is_valid,\n- \"index\":self.index\n+ \"index\": self.index\n }\n \n+\n class ModelField(Validator):\n \n index = 1\n- def __init__(self,name=None,save=True, *args, **kwargs):\n- self.name = name \n+\n+ def __init__(self, name=None, save=True, *args, **kwargs):\n+ self.name = name\n self.save = save\n super().__init__(*args, **kwargs)\n \n def to_json(self):\n result = super().to_json()\n result['name'] = self.name\n- return result \n+ return result\n \n \n class CreatedField(ModelField):\n- \n+\n @property\n def initial_value(self):\n return now()\n@@ -136,67 +168,99 @@ class CreatedField(ModelField):\n if not self.value:\n self.value = now()\n \n+\n class UpdatedField(ModelField):\n \n def update(self):\n self.value = now()\n \n+\n class DeletedField(ModelField):\n \n def update(self):\n self.value = now()\n \n+\n class UUIDField(ModelField):\n- \n- @property \n+\n+ @property\n def initial_value(self):\n return str(uuid.uuid4())\n \n \n class BaseModel:\n+\n+ uid = UUIDField(name=\"uid\", required=True)\n+ created_at = CreatedField(name=\"created_at\", required=True, regex=TIMESTAMP_REGEX, place_holder=\"Created at\")\n+ updated_at = UpdatedField(name=\"updated_at\", regex=TIMESTAMP_REGEX, place_holder=\"Updated at\")\n+ deleted_at = DeletedField(name=\"deleted_at\", regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\")\n+\n+ @classmethod \n+ def from_record(cls, record, mapper):\n+ model = cls.__new__()\n+ model.mapper = mapper \n+ model.record = record\n+ return model\n+\n+ @property \n+ def mapper(self):\n+ return self._mapper \n+\n+ @mapper.setter \n+ def mapper(self, value):\n+ self._mapper = value \n+\n+ @property \n+ def record(self):\n+ return {field.name: field.value for field in self.fields}\n \n- uid = UUIDField(name=\"uid\",required=True)\n- created_at = CreatedField(name=\"created_at\",required=True, regex=TIMESTAMP_REGEX, place_holder=\"Created at\")\n- updated_at = UpdatedField(name=\"updated_at\",regex=TIMESTAMP_REGEX,place_holder=\"Updated at\")\n- deleted_at = DeletedField(name=\"deleted_at\",regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\")\n+ @record.setter \n+ def record(self, value):\n+ for key, value in self._record.items():\n+ field = self.fields.get(key)\n+ if not field:\n+ continue\n+ field.value = value\n+ return self\n \n- \n def __init__(self, *args, **kwargs):\n print(self.__dict__)\n print(dir(self.__class__))\n+ self._mapper = None\n self.fields = {}\n for key in dir(self.__class__):\n- obj = getattr(self.__class__,key)\n+ obj = getattr(self.__class__, key)\n \n- if isinstance(obj,Validator):\n+ if isinstance(obj, Validator):\n self.__dict__[key] = copy.deepcopy(obj)\n print(\"JAAA\")\n- self.__dict__[key].value = kwargs.pop(key,self.__dict__[key].initial_value)\n+ self.__dict__[key].value = kwargs.pop(key, self.__dict__[key].initial_value)\n self.fields[key] = self.__dict__[key]\n \n def __setitem__(self, key, value):\n obj = self.__dict__.get(key)\n- if isinstance(obj,Validator):\n- obj.value = value \n+ if isinstance(obj, Validator):\n+ obj.value = value\n \n def __getattr__(self, key):\n obj = self.__dict__.get(key)\n- if isinstance(obj,Validator):\n+ if isinstance(obj, Validator):\n print(\"HPAPP\")\n- return obj.value \n+ return obj.value\n return obj\n \n def set_user_data(self, data):\n for key, value in data.items():\n field = self.fields.get(key)\n if not field:\n- continue \n+ continue\n if value.get('name'):\n value = value.get('value')\n field.value = value\n- \n \n- @property \n+ \n+\n+ @property\n def is_valid(self):\n for field in self.fields.values():\n if not field.is_valid:\n@@ -205,46 +269,44 @@ class BaseModel:\n \n def __getitem__(self, key):\n obj = self.__dict__.get(key)\n- if isinstance(obj,Validator):\n- return obj.value \n+ if isinstance(obj, Validator):\n+ return obj.value\n \n def __setattr__(self, key, value):\n- obj = getattr(self,key)\n- if isinstance(obj,Validator):\n+ obj = getattr(self, key)\n+ if isinstance(obj, Validator):\n obj.value = value\n else:\n- @property \n+ self.__dict__[key] = value\n+\n+ @property\n def record(self):\n obj = self.to_json()\n record = {}\n- for key,value in obj.items():\n- if getattr(self,key).save:\n+ for key, value in obj.items():\n+ if getattr(self, key).save:\n record[key] = value.get('value')\n return record\n \n- def to_json(self,encode=False):\n+ def to_json(self, encode=False):\n model_data = OrderedDict({\n \"uid\": self.uid.value,\n \"created_at\": self.created_at.value,\n \"updated_at\": self.updated_at.value,\n \"deleted_at\": self.deleted_at.value\n })\n- \n- for key,value in self.__dict__.items(): \n+\n+ for key, value in self.__dict__.items():\n if key == \"record\":\n continue\n value = self.__dict__[key]\n- if hasattr(value,\"value\"):\n+ if hasattr(value, \"value\"):\n model_data[key] = value.to_json()\n if encode:\n- return json.dumps(model_data,indent=2)\n+ return json.dumps(model_data, indent=2)\n return model_data\n \n+\n class FormElement(ModelField):\n \n def __init__(self, place_holder=None, *args, **kwargs):\n@@ -252,15 +314,14 @@ class FormElement(ModelField):\n self.place_holder = place_holder\n \n \n-\n class FormElement(ModelField):\n \n- def __init__(self,place_holder=None, *args, **kwargs): \n- self.place_holder = place_holder \n+ def __init__(self, place_holder=None, *args, **kwargs):\n+ self.place_holder = place_holder\n super().__init__(*args, **kwargs)\n \n def to_json(self):\n data = super().to_json()\n- data[\"name\"] = self.name \n+ data[\"name\"] = self.name\n data[\"place_holder\"] = self.place_holder\n- return data \n+ return data\n\\ No newline at end of file\ndiff --git a/src/snek/system/security.py b/src/snek/system/security.py\nnew file mode 100644\nindex 0000000..b319f54\n--- /dev/null\n+++ b/src/snek/system/security.py\n@@ -0,0 +1,20 @@\n+import hashlib \n+\n+DEFAULT_SALT = b\"snekker-de-snek-\"\n+\n+async def hash(data,salt=DEFAULT_SALT):\n+ try:\n+ data = data.encode(errors=\"ignore\")\n+ except AttributeError:\n+ pass \n+ try:\n+ salt = salt.encode(errors=\"ignore\")\n+ except AttributeError:\n+ pass\n+ salted = salt + data\n+\n+ obj = hashlib.sha256(salted)\n+ return obj.hexdigest()\n+\n+async def verify(string:str, hashed:str):\n+ return await hash(string) == hashed \ndiff --git a/src/snek/system/service.py b/src/snek/system/service.py\nnew file mode 100644\nindex 0000000..5a8b553\n--- /dev/null\n+++ b/src/snek/system/service.py\n@@ -0,0 +1,40 @@\n+\n+\n+\n+from snek.mapper import get_mapper\n+from snek.system.mapper import BaseMapper \n+from snek.model.user import UserModel\n+\n+class BaseService:\n+\n+ mapper_name:BaseMapper = None\n+\n+ def __init__(self, app):\n+ self.app = app \n+ if self.mapper_name:\n+ self.mapper = get_mapper(self.mapper_name, app=self.app)\n+ else:\n+ self.mapper = None \n+\n+ async def exists(self, **kwargs):\n+ return self.mapper.exists(**kwargs)\n+ \n+ async def count(self, **kwargs):\n+ return self.mapper.count(**kwargs)\n+\n+ async def new(self, **kwargs):\n+ return await self.mapper.new()\n+\n+ async def get(self, **kwargs):\n+ return await self.mapper.get(**kwargs)\n+ \n+ async def save(self, model:UserModel):\n+ if model.is_valid:\n+ return self.mapper.save(model) and True \n+ return False \n+ \n+ async def find(self, **kwargs):\n+ return await self.mapper.find(**kwargs)\n+ \n+ async def delete(self, **kwargs):\n+ return await self.mapper.delete(**kwargs)\n\\ No newline at end of file\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nnew file mode 100644\nindex 0000000..458aa20\n--- /dev/null\n+++ b/src/snek/system/view.py\n@@ -0,0 +1,38 @@\n+from aiohttp import web\n+\n+from snek.system.markdown import render_markdown \n+\n+class BaseView(web.View):\n+ \n+ @property \n+ def app(self):\n+ return self.request.app\n+ \n+ @property\n+ def db(self):\n+ return self.app.db\n+\n+ async def json_response(self, data):\n+ return web.json_response(data)\n+\n+ async def render_template(self, template_name, context=None):\n+ if template_name.endswith(\".md\"):\n+ response = await self.request.app.render_template(template_name,self.request,context)\n+ body = await render_markdown(self.app, response.body.decode())\n+ return web.Response(body=body,content_type=\"text/html\")\n+ return await self.request.app.render_template(template_name, self.request,context)\n+ \n+class BaseFormView(BaseView):\n+\n+ form = None \n+\n+ async def get(self):\n+ form = self.form()\n+ return await self.json_response(form.to_json())\n+ \n+ async def post(self):\n+ form = self.form()\n+ post = await self.request.json()\n+ form.set_user_data(post['form'])\n+ return await self.json_response(form.to_json()) \n+\ndiff --git a/src/snek/templates/about.html b/src/snek/templates/about.html\nnew file mode 100644\nindex 0000000..0f1b8a9\n--- /dev/null\n+++ b/src/snek/templates/about.html\n@@ -0,0 +1,7 @@\n+{% extends \"base.html\" %}\n+\n+{% block main %}\n+\n+\n+\n+{% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/templates/about.md b/src/snek/templates/about.md\nnew file mode 100644\nindex 0000000..134fbc9\n--- /dev/null\n+++ b/src/snek/templates/about.md\n@@ -0,0 +1,15 @@\n+\n+A snek is a danger noodle.\n+\n+I made several design choices: \n+- Implemented **the worst 3rd party markdown to html renderer ever**. See this nice *bullet list*.\n+ - Only password requirement is thats it requires six characters. Users are responsibly for their own security. Snek is not so arrogant to determine if a password is strong enough. It's up to what user prefers. Snek does not have a forgot-my-password service tho.\n+ - Email is not required for registration. Email is (maybe) used in future for resetting password.\n+ - Homebrew made ORM framework based on dataset.\n+ - Homebrew made Form framework based on the homebrew made ORM. Most forms are ModelForms but always require an service to be saved for sake of consistency and structure.\n+ - !DRY for HMTL/jinja2 templates. For templates Snek does prefer to repeat itself to implement exceptions for a page easier. For Snek it's preffered do a few updates instead of maintaining a complex generic system that requires maintenance regarding templates.\n+ - No existing chat backend like `inspircd` (Popular decent IRC server written in the language of angels) because I prefer to know what is exactly going on above performance and concurrency limit. Also, this approach reduces as networking layer / gateway layer.\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex 5b1bdf2..9f57467 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -4,9 +4,8 @@\n \n \n {% block title %}{% endblock %}\n+ \n \n- \n- \n \n \n \ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nindex 1ee1f77..c2200fb 100644\n--- a/src/snek/templates/index.html\n+++ b/src/snek/templates/index.html\n@@ -14,7 +14,8 @@\n \n Or\n \n-\n+ Design choices\n+ See web Application so far\n \n \n \ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex c09ec70..c70d429 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -1,5 +1,5 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n- \n+ \n {% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex 61da961..f0a82e0 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -1,5 +1,5 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n- \n+ \n {% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/view/about.py b/src/snek/view/about.py\nnew file mode 100644\nindex 0000000..593d5a9\n--- /dev/null\n+++ b/src/snek/view/about.py\n@@ -0,0 +1,14 @@\n+\n+\n+from snek.system.view import BaseView\n+\n+\n+class AboutHTMLView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"about.html\")\n+ \n+class AboutMDView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"about.md\")\n\\ No newline at end of file\ndiff --git a/src/snek/view/base.py b/src/snek/view/base.py\ndeleted file mode 100644\nindex d962ee5..0000000\n--- a/src/snek/view/base.py\n+++ /dev/null\n@@ -1,31 +0,0 @@\n-from aiohttp import web \n-\n-class BaseView(web.View):\n- \n- @property \n- def app(self):\n- return self.request.app\n- \n- @property\n- def db(self):\n- return self.app.db\n-\n- def json_response(self, data):\n- return web.json_response(data)\n-\n- def render_template(self, template_name, context=None):\n- return self.request.app.render_template(template_name, self.request,context)\n- \n-class BaseFormView(BaseView):\n-\n- form = None \n-\n- async def get(self):\n- form = self.form()\n- return self.json_response(form.to_json())\n- \n- async def post(self):\n- form = self.form()\n- post = await self.request.json()\n- form.set_user_data(post['form'])\n- return self.json_response(form.to_json()) \n\\ No newline at end of file\ndiff --git a/src/snek/view/index.py b/src/snek/view/index.py\nindex a5d8b92..c7861fa 100644\n--- a/src/snek/view/index.py\n+++ b/src/snek/view/index.py\n@@ -1,4 +1,4 @@\n-from snek.view.base import BaseView\n+from snek.system.view import BaseView\n \n class IndexView(BaseView):\n \ndiff --git a/src/snek/view/login.py b/src/snek/view/login.py\nindex 3a3beaf..ffedc79 100644\n--- a/src/snek/view/login.py\n+++ b/src/snek/view/login.py\n@@ -1,5 +1,5 @@\n from snek.form.register import RegisterForm\n-from snek.view.base import BaseView \n+from snek.system.view import BaseView\n \n class LoginView(BaseView):\n \ndiff --git a/src/snek/view/login_form.py b/src/snek/view/login_form.py\nindex 26527da..e9b6eac 100644\n--- a/src/snek/view/login_form.py\n+++ b/src/snek/view/login_form.py\n@@ -1,4 +1,4 @@\n-from snek.view.base import BaseFormView\n+from snek.system.view import BaseFormView\n from snek.form.login import LoginForm\n \n class LoginFormView(BaseFormView):\ndiff --git a/src/snek/view/register.py b/src/snek/view/register.py\nindex 095b7a3..e3b3038 100644\n--- a/src/snek/view/register.py\n+++ b/src/snek/view/register.py\n@@ -1,4 +1,4 @@\n-from snek.view.base import BaseView \n+from snek.system.view import BaseView\n \n class RegisterView(BaseView):\n \ndiff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py\nindex 0ae7630..8099b01 100644\n--- a/src/snek/view/register_form.py\n+++ b/src/snek/view/register_form.py\n@@ -1,5 +1,5 @@\n from snek.form.register import RegisterForm\n-from snek.view.base import BaseFormView \n+from snek.system.view import BaseFormView\n \n class RegisterFormView(BaseFormView):\n form = RegisterForm\n\\ No newline at end of file\ndiff --git a/src/snek/view/view.py b/src/snek/view/view.py\ndeleted file mode 100644\nindex ea642a3..0000000\n--- a/src/snek/view/view.py\n+++ /dev/null\n@@ -1,6 +0,0 @@\n-from snek.view.base import BaseView \n-\n-class WebView(BaseView):\n-\n- async def get(self):\n- return await self.render_template(\"web.html\")\n\\ No newline at end of file\ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nnew file mode 100644\nindex 0000000..b06563a\n--- /dev/null\n+++ b/src/snek/view/web.py\n@@ -0,0 +1,6 @@\n+from snek.system.view import BaseView\n+\n+class WebView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"web.html\")\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Use \"/back\" URL for back button", "commit": "4b48485bcc9f73662a1eb4ab3142bb0f4d5bef7a", "diff": "commit 4b48485bcc9f73662a1eb4ab3142bb0f4d5bef7a\nAuthor: retoor \nDate: Fri Jan 24 14:05:47 2025 +0100\n\n Complete system.\n\ndiff --git a/src/snek/static/fancy-button.js b/src/snek/static/fancy-button.js\nindex 5407a3b..a3e28f8 100644\n--- a/src/snek/static/fancy-button.js\n+++ b/src/snek/static/fancy-button.js\n@@ -44,7 +44,9 @@ class FancyButton extends HTMLElement {\n const me = this \n this.buttonElement.appendChild(document.createTextNode(this.getAttribute(\"text\")))\n this.buttonElement.addEventListener(\"click\",()=>{\n- if(me.url){\n+ if(me.url == \"/back\" || me.url == \"/back/\"){\n+ window.history.back()\n+ }else if(me.url){\n window.location = me.url\n }\n })\ndiff --git a/src/snek/templates/about.html b/src/snek/templates/about.html\nindex 0f1b8a9..e403458 100644\n--- a/src/snek/templates/about.html\n+++ b/src/snek/templates/about.html\n@@ -3,5 +3,5 @@\n {% block main %}\n \n \n-\n+\n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Implement size attribute for fancy-button", "commit": "0271e3f9719a4155fc5be37c36feca865932b0c1", "diff": "commit 0271e3f9719a4155fc5be37c36feca865932b0c1\nAuthor: retoor \nDate: Fri Jan 24 14:15:55 2025 +0100\n\n Complete system.\n\ndiff --git a/src/snek/static/fancy-button.js b/src/snek/static/fancy-button.js\nindex a3e28f8..5eec215 100644\n--- a/src/snek/static/fancy-button.js\n+++ b/src/snek/static/fancy-button.js\n@@ -7,7 +7,18 @@ class FancyButton extends HTMLElement {\n constructor(){\n super()\n this.attachShadow({mode:'open'})\n+ }\n+\n+ connectedCallback() {\n+\n this.container = document.createElement('span')\n+ let size = this.getAttribute('size')\n+ console.info({GG:size})\n+ if(size == 'auto'){\n+ size = '1%' \n+ }else{\n+ size = '33%'\n+ }\n this.styleElement = document.createElement(\"style\")\n this.styleElement.innerHTML = `\n :root {\n@@ -16,7 +27,7 @@ class FancyButton extends HTMLElement {\n }\n button {\n width: var(--width);\n- min-width: 33%;\n+ min-width: ${size};\n padding: 10px;\n border: none;\n@@ -26,19 +37,20 @@ class FancyButton extends HTMLElement {\n font-weight: bold;\n cursor: pointer;\n transition: background-color 0.3s;\n+\n }\n button:hover {\n }\n `\n this.container.appendChild(this.styleElement)\n this.buttonElement = document.createElement('button')\n this.container.appendChild(this.buttonElement)\n this.shadowRoot.appendChild(this.container)\n- }\n-\n- connectedCallback() {\n+ \n this.url = this.getAttribute('url');\n this.value = this.getAttribute('value')\n const me = this \ndiff --git a/src/snek/templates/about.html b/src/snek/templates/about.html\nindex e403458..debba2a 100644\n--- a/src/snek/templates/about.html\n+++ b/src/snek/templates/about.html\n@@ -3,5 +3,5 @@\n {% block main %}\n \n \n-\n+\n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "fix: Updated login and register button URLs", "commit": "bda93e354f4691483dbbef29949672ab0989b7e0", "diff": "commit bda93e354f4691483dbbef29949672ab0989b7e0\nAuthor: retoor \nDate: Fri Jan 24 14:16:52 2025 +0100\n\n Complete system.\n\ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nindex c2200fb..8157f3d 100644\n--- a/src/snek/templates/index.html\n+++ b/src/snek/templates/index.html\n@@ -11,9 +11,9 @@\n \n
\n

Snek

\n- \n+ \n Or\n- \n+ \n Design choices\n See web Application so far\n
"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Style adjustments and responsive design improvements", "commit": "757b67b78c2f396df7ac7b5706f98833aedfb85b", "diff": "commit 757b67b78c2f396df7ac7b5706f98833aedfb85b\nAuthor: retoor \nDate: Fri Jan 24 14:47:19 2025 +0100\n\n CSS.\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex 363e4f1..263f1eb 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -4,6 +4,7 @@\n box-sizing: border-box;\n }\n \n+\n body {\n font-family: Arial, sans-serif;\n@@ -12,6 +13,10 @@ body {\n display: flex;\n flex-direction: column;\n height: 100vh;\n+ min-width: 100%;\n+}\n+main {\n+ min-width: 100%;\n }\n \n header {\ndiff --git a/src/snek/static/style.css b/src/snek/static/style.css\nindex 990fcf9..008a3ee 100644\n--- a/src/snek/static/style.css\n+++ b/src/snek/static/style.css\n@@ -1,3 +1,27 @@\n+\n+* {\n+ margin: 0;\n+ padding: 0;\n+ box-sizing: border-box;\n+ }\n+\n+.center {\n+ padding: 30px;\n+ \n+ text-align: center;\n+ margin: auto auto;\n+ left: 25%;\n+ position: absolute;\n+ }\n+\n+ @media screen and (max-width: 500px) {\n+ .center {\n+ width: 100%;\n+ left: 0px;\n+ }\n+ \n+ }\n+\n h1 {\n font-size: 2em;\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex 9f57467..8637867 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -18,11 +18,6 @@\n \n
\n
\n- \n {% block main %}\n {% endblock %}\n
\ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex c70d429..2fe1a73 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -1,5 +1,7 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n- \n+\n+ \n+\n {% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex f0a82e0..3668f13 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -1,5 +1,5 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n- \n+ \n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor styling and layout for improved UI consistency", "commit": "6ba6121988dae019d4c4c0a3b8592b443f094065", "diff": "commit 6ba6121988dae019d4c4c0a3b8592b443f094065\nAuthor: retoor \nDate: Fri Jan 24 15:20:35 2025 +0100\n\n CSS.\n\ndiff --git a/src/snek/form/login.py b/src/snek/form/login.py\nindex a87f7d3..910cd73 100644\n--- a/src/snek/form/login.py\n+++ b/src/snek/form/login.py\n@@ -21,4 +21,5 @@ class LoginForm(Form):\n text=\"Login\",\n type=\"button\"\n )\n+ \n \ndiff --git a/src/snek/static/html-frame.js b/src/snek/static/html-frame.js\nindex 0d5d4c9..75f800e 100644\n--- a/src/snek/static/html-frame.js\n+++ b/src/snek/static/html-frame.js\n@@ -30,9 +30,7 @@ class HTMLFrame extends HTMLElement {\n const parent = this\n const markdownElement = document.createElement('div')\n markdownElement.innerHTML = html\n- document.body.appendChild(markdownElement)\n- \n+ this.outerHTML = html\n }else{\n this.container.innerHTML = html;\n }\ndiff --git a/src/snek/static/style.css b/src/snek/static/style.css\nindex 008a3ee..d2cda8c 100644\n--- a/src/snek/static/style.css\n+++ b/src/snek/static/style.css\n@@ -1,18 +1,18 @@\n \n * {\n- margin: 0;\n- padding: 0;\n+ \n box-sizing: border-box;\n }\n \n-.center {\n+ .dialog {\n+\n+ border-radius: 10px;\n padding: 30px;\n- \n- text-align: center;\n- margin: auto auto;\n- left: 25%;\n- position: absolute;\n- }\n+ width: 800px;\n+ margin: 30px;\n+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n+}\n \n @media screen and (max-width: 500px) {\n .center {\n@@ -34,8 +34,15 @@ h2 {\n margin-bottom: 20px;\n }\n body {\n+ font-family: Arial, sans-serif;\n+ line-height: 1.5;\n+ display: flex;\n+ flex-direction: column;\n+ justify-content: center;\n+ align-items: center;\n+ min-height: 100vh;\n \n }\n div {\ndiff --git a/src/snek/templates/about.html b/src/snek/templates/about.html\nindex debba2a..b68396e 100644\n--- a/src/snek/templates/about.html\n+++ b/src/snek/templates/about.html\n@@ -1,7 +1,10 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n+
\n \n+ \n \n-\n+\n+
\n {% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex 2fe1a73..3e72928 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -1,7 +1,7 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n-\n+ \n \n \n {% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex 3668f13..b1540ea 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -1,5 +1,7 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n+\n+ \n \n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor login and register forms to use /login.json and /register.json endpoints.", "commit": "c1eeacc0b415ef770418bb053d18ac0ffa4f64c2", "diff": "commit c1eeacc0b415ef770418bb053d18ac0ffa4f64c2\nAuthor: retoor \nDate: Fri Jan 24 16:08:56 2025 +0100\n\n Caching.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex deac5d3..db9ebf9 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -2,7 +2,7 @@ import pathlib\n \n from aiohttp import web\n from app.app import Application as BaseApplication\n-\n+from app.cache import time_cache_async\n from jinja_markdown2 import MarkdownExtension\n from snek.system import http\n from snek.system.middleware import cors_middleware\n@@ -41,10 +41,9 @@ class Application(BaseApplication):\n self.router.add_view(\"/about.md\", AboutMDView)\n self.router.add_view(\"/web.html\", WebView)\n self.router.add_view(\"/login.html\", LoginView)\n- self.router.add_view(\"/login-form.json\", LoginFormView)\n+ self.router.add_view(\"/login.json\", LoginFormView)\n self.router.add_view(\"/register.html\", RegisterView)\n- \n- self.router.add_view(\"/register-form.json\", RegisterFormView)\n+ self.router.add_view(\"/register.json\", RegisterFormView)\n self.router.add_get(\"/http-get\", self.handle_http_get)\n self.router.add_get(\"/http-photo\", self.handle_http_photo)\n \n@@ -65,6 +64,11 @@ class Application(BaseApplication):\n return web.Response(\n body=path.read_bytes(), headers={\"Content-Type\": \"image/png\"}\n )\n+ \n+ \n+ @time_cache_async(60)\n+ async def render_template(self, template, request, context=None):\n+ return await super().render_template(template, request, context)\n \n \n app = Application()\ndiff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py\nindex bded949..93bc08c 100644\n--- a/src/snek/system/markdown.py\n+++ b/src/snek/system/markdown.py\n@@ -8,7 +8,7 @@ from pygments import highlight\n from pygments.lexers import get_lexer_by_name\n from pygments.formatters import html\n from pygments.styles import get_style_by_name\n-\n+import functools\n \n class MarkdownRenderer(HTMLRenderer):\n def __init__(self, app, template):\n@@ -37,7 +37,11 @@ class MarkdownRenderer(HTMLRenderer):\n return markdown(markdown_string)\n \n \n+@functools.cache\n+def render_markdown_sync(app, markdown_string):\n+ renderer = MarkdownRenderer(app,None)\n+ markdown = Markdown(renderer=renderer)\n+ return markdown(markdown_string)\n+\n async def render_markdown(app, markdown_string):\n- renderer = MarkdownRenderer(app,None)\n- markdown = Markdown(renderer=renderer)\n- return markdown(markdown_string)\n\\ No newline at end of file\n+ return render_markdown_sync(app,markdown_string)\n\\ No newline at end of file\ndiff --git a/src/snek/system/model.py b/src/snek/system/model.py\nindex eb490b3..9a00186 100644\n--- a/src/snek/system/model.py\n+++ b/src/snek/system/model.py\n@@ -81,8 +81,6 @@ class Validator:\n self.regex = regex\n self._value = None\n self.value = value\n- print(\"xxxx\", value, flush=True)\n-\n self.kind = kind\n self.help_text = help_text\n self.__dict__.update(kwargs)\n@@ -106,7 +104,6 @@ class Validator:\n error_list.append(f\"Field should be minimal {self.min_length} characters long.\")\n if self.max_length is not None and len(self.value) > self.max_length:\n error_list.append(f\"Field should be maximal {self.max_length} characters long.\")\n- print(self.regex, self.value, flush=True)\n if self.regex and self.value and not re.match(self.regex, self.value):\n error_list.append(\"Invalid value.\")\n if self.kind and not isinstance(self.value, self.kind):\n@@ -224,8 +221,6 @@ class BaseModel:\n return self\n \n def __init__(self, *args, **kwargs):\n- print(self.__dict__)\n- print(dir(self.__class__))\n self._mapper = None\n self.fields = {}\n for key in dir(self.__class__):\n@@ -233,7 +228,6 @@ class BaseModel:\n \n if isinstance(obj, Validator):\n self.__dict__[key] = copy.deepcopy(obj)\n- print(\"JAAA\")\n self.__dict__[key].value = kwargs.pop(key, self.__dict__[key].initial_value)\n self.fields[key] = self.__dict__[key]\n \n@@ -245,7 +239,6 @@ class BaseModel:\n def __getattr__(self, key):\n obj = self.__dict__.get(key)\n if isinstance(obj, Validator):\n- print(\"HPAPP\")\n return obj.value\n return obj\n \ndiff --git a/src/snek/templates/about.md b/src/snek/templates/about.md\nindex 134fbc9..c28e9d0 100644\n--- a/src/snek/templates/about.md\n+++ b/src/snek/templates/about.md\n@@ -13,3 +13,57 @@ I made several design choices:\n - Homebrew made Form framework based on the homebrew made ORM. Most forms are ModelForms but always require an service to be saved for sake of consistency and structure.\n - !DRY for HMTL/jinja2 templates. For templates Snek does prefer to repeat itself to implement exceptions for a page easier. For Snek it's preffered do a few updates instead of maintaining a complex generic system that requires maintenance regarding templates.\n - No existing chat backend like `inspircd` (Popular decent IRC server written in the language of angels) because I prefer to know what is exactly going on above performance and concurrency limit. Also, this approach reduces as networking layer / gateway layer.\n+\n+\n+A few examples of how the system framework works.\n+\n+```python\n+\n+new_user_object = await app.service.user.register(\n+ username=\"retoor\", \n+ password=\"retoorded\"\n+)\n+```\n+\n+```python\n+from snek.system import security\n+\n+var1 = security.encrypt(\"data\")\n+var2 = security.encrypt(b\"data\")\n+\n+assert(var1 == var2)\n+```\n+\n+```python\n+from snek.system.view import BaseView \n+\n+class IndexView(BaseView):\n+ \n+ async def get(self):\n+ return await self.render(\"index.html\")\n+```\n+```python\n+from snek.system.view import BaseFormView\n+from snek.form.register import RegisterForm\n+\n+class RegisterFormView(BaseFormView):\n+ \n+ form = RegisterForm\n+```\n+```python\n+app.routes.add_view(\"/your-page.html\", YourViewClass)\n+```\n\\ No newline at end of file\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex 8637867..e00c886 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -4,6 +4,7 @@\n \n \n {% block title %}{% endblock %}\n+ \n \n \n \ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex 3e72928..0a6bcc1 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -2,6 +2,6 @@\n \n {% block main %}\n \n- \n+ \n \n {% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex b1540ea..f8d1067 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -3,5 +3,5 @@\n {% block main %}\n \n \n- \n+ \n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Implemented caching decorator for asynchronous functions", "commit": "21ab5628b072320ae0851819116e57539b2397d9", "diff": "commit 21ab5628b072320ae0851819116e57539b2397d9\nAuthor: retoor \nDate: Fri Jan 24 16:09:10 2025 +0100\n\n Caching.\n\ndiff --git a/src/snek/system/cache.py b/src/snek/system/cache.py\nnew file mode 100644\nindex 0000000..2992803\n--- /dev/null\n+++ b/src/snek/system/cache.py\n@@ -0,0 +1,17 @@\n+\n+import functools \n+\n+cache = functools.cache\n+\n+def async_cache(func):\n+ cache = {}\n+\n+ @functools.wraps(func)\n+ async def wrapper(*args):\n+ if args in cache:\n+ return cache[args]\n+ result = await func(*args)\n+ cache[args] = result\n+ return result\n+\n+ return wrapper\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Switch to Gunicorn and add API documentation", "commit": "8486c22c325ba358bd48766c518f7c7bd30059eb", "diff": "commit 8486c22c325ba358bd48766c518f7c7bd30059eb\nAuthor: retoor \nDate: Fri Jan 24 16:33:27 2025 +0100\n\n Disabled cache.\n\ndiff --git a/compose.yml b/compose.yml\nindex 3b1f650..24e186c 100644\n--- a/compose.yml\n+++ b/compose.yml\n@@ -6,5 +6,6 @@ services:\n - \"8081:8081\"\n volumes:\n - ./:/code\n- entrypoint: [\"python\",\"-m\",\"snek.app\"]\n+ entrypoint: [\"gunicorn\", \"-w\", \"1\", \"-k\", \"aiohttp.worker.GunicornWebWorker\", \"snek.gunicorn:app\",\"--bind\",\"0.0.0.0:8081\"]\n \n\\ No newline at end of file\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex db9ebf9..f25ffba 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -7,6 +7,7 @@ from jinja_markdown2 import MarkdownExtension\n from snek.system import http\n from snek.system.middleware import cors_middleware\n from snek.view.about import AboutHTMLView, AboutMDView\n+from snek.view.docs import DocsHTMLView, DocsMDView\n from snek.view.index import IndexView\n from snek.view.login import LoginView\n from snek.view.login_form import LoginFormView\n@@ -39,6 +40,9 @@ class Application(BaseApplication):\n )\n self.router.add_view(\"/about.html\", AboutHTMLView)\n self.router.add_view(\"/about.md\", AboutMDView)\n+ self.router.add_view(\"/docs.html\", DocsHTMLView)\n+ self.router.add_view(\"/docs.md\", DocsMDView)\n+ \n self.router.add_view(\"/web.html\", WebView)\n self.router.add_view(\"/login.html\", LoginView)\n self.router.add_view(\"/login.json\", LoginFormView)\n@@ -66,7 +70,7 @@ class Application(BaseApplication):\n )\n \n \n- @time_cache_async(60)\n async def render_template(self, template, request, context=None):\n return await super().render_template(template, request, context)\n \ndiff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py\nindex 93bc08c..489a3e5 100644\n--- a/src/snek/system/markdown.py\n+++ b/src/snek/system/markdown.py\n@@ -9,6 +9,7 @@ from pygments.lexers import get_lexer_by_name\n from pygments.formatters import html\n from pygments.styles import get_style_by_name\n import functools\n+from app.cache import time_cache_async\n \n class MarkdownRenderer(HTMLRenderer):\n def __init__(self, app, template):\n@@ -28,7 +29,6 @@ class MarkdownRenderer(HTMLRenderer):\n lexer = get_lexer_by_name(lang, stripall=True)\n formatter = html.HtmlFormatter(lineseparator=\"
\")\n- print(code, lang,info, flush=True)\n return highlight(code, lexer, formatter)\n def render(self):\n markdown_string = self.app.template_path.joinpath(self.template).read_text()\n@@ -37,11 +37,12 @@ class MarkdownRenderer(HTMLRenderer):\n return markdown(markdown_string)\n \n \n-@functools.cache\n+\n def render_markdown_sync(app, markdown_string):\n renderer = MarkdownRenderer(app,None)\n markdown = Markdown(renderer=renderer)\n return markdown(markdown_string)\n \n+@time_cache_async(120)\n async def render_markdown(app, markdown_string):\n return render_markdown_sync(app,markdown_string)\n\\ No newline at end of file\ndiff --git a/src/snek/templates/about.md b/src/snek/templates/about.md\nindex c28e9d0..0b43fb4 100644\n--- a/src/snek/templates/about.md\n+++ b/src/snek/templates/about.md\n@@ -14,56 +14,3 @@ I made several design choices:\n - !DRY for HMTL/jinja2 templates. For templates Snek does prefer to repeat itself to implement exceptions for a page easier. For Snek it's preffered do a few updates instead of maintaining a complex generic system that requires maintenance regarding templates.\n - No existing chat backend like `inspircd` (Popular decent IRC server written in the language of angels) because I prefer to know what is exactly going on above performance and concurrency limit. Also, this approach reduces as networking layer / gateway layer.\n \n-\n-A few examples of how the system framework works.\n-\n-```python\n-\n-new_user_object = await app.service.user.register(\n- username=\"retoor\", \n- password=\"retoorded\"\n-)\n-```\n-\n-```python\n-from snek.system import security\n-\n-var1 = security.encrypt(\"data\")\n-var2 = security.encrypt(b\"data\")\n-\n-assert(var1 == var2)\n-```\n-\n-```python\n-from snek.system.view import BaseView \n-\n-class IndexView(BaseView):\n- \n- async def get(self):\n- return await self.render(\"index.html\")\n-```\n-```python\n-from snek.system.view import BaseFormView\n-from snek.form.register import RegisterForm\n-\n-class RegisterFormView(BaseFormView):\n- \n- form = RegisterForm\n-```\n-```python\n-app.routes.add_view(\"/your-page.html\", YourViewClass)\n-```\n\\ No newline at end of file\ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nindex 8157f3d..ad02a6a 100644\n--- a/src/snek/templates/index.html\n+++ b/src/snek/templates/index.html\n@@ -15,7 +15,8 @@\n Or\n \n Design choices\n- See web Application so far\n+ App preview\n+ API docs\n \n \n "} +{"repo": ".", "date": "2025-01-24", "line": "feat: Add documentation pages and views", "commit": "aecd9f844ef0a277a55aa536db3336362e8db353", "diff": "commit aecd9f844ef0a277a55aa536db3336362e8db353\nAuthor: retoor \nDate: Fri Jan 24 16:34:02 2025 +0100\n\n Docs.\n\ndiff --git a/src/snek/templates/docs.html b/src/snek/templates/docs.html\nnew file mode 100644\nindex 0000000..21c0309\n--- /dev/null\n+++ b/src/snek/templates/docs.html\n@@ -0,0 +1,10 @@\n+{% extends \"base.html\" %}\n+\n+{% block main %}\n+
\n+\n+ \n+\n+\n+
\n+{% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/templates/docs.md b/src/snek/templates/docs.md\nnew file mode 100644\nindex 0000000..2cf60cb\n--- /dev/null\n+++ b/src/snek/templates/docs.md\n@@ -0,0 +1,53 @@\n+\n+Currently only some details about the internal API are available.\n+\n+```python\n+\n+new_user_object = await app.service.user.register(\n+ username=\"retoor\", \n+ password=\"retoorded\"\n+)\n+```\n+\n+```python\n+from snek.system import security\n+\n+var1 = security.encrypt(\"data\")\n+var2 = security.encrypt(b\"data\")\n+\n+assert(var1 == var2)\n+```\n+\n+```python\n+from snek.system.view import BaseView \n+\n+class IndexView(BaseView):\n+ \n+ async def get(self):\n+ return await self.render(\"index.html\")\n+```\n+```python\n+from snek.system.view import BaseFormView\n+from snek.form.register import RegisterForm\n+\n+class RegisterFormView(BaseFormView):\n+ \n+ form = RegisterForm\n+```\n+```python\n+app.routes.add_view(\"/your-page.html\", YourViewClass)\n+```\n\\ No newline at end of file\ndiff --git a/src/snek/view/docs.py b/src/snek/view/docs.py\nnew file mode 100644\nindex 0000000..e69d754\n--- /dev/null\n+++ b/src/snek/view/docs.py\n@@ -0,0 +1,15 @@\n+\n+\n+\n+from snek.system.view import BaseView\n+\n+\n+class DocsHTMLView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"docs.html\")\n+ \n+class DocsMDView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"docs.md\")\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "docs: Added styling for dialog elements", "commit": "be9489f939b3518f9c3a73b9e54ba0f9d34ae24c", "diff": "commit be9489f939b3518f9c3a73b9e54ba0f9d34ae24c\nAuthor: retoor \nDate: Fri Jan 24 16:43:57 2025 +0100\n\n Docs.\n\ndiff --git a/src/snek/static/style.css b/src/snek/static/style.css\nindex d2cda8c..63a28ed 100644\n--- a/src/snek/static/style.css\n+++ b/src/snek/static/style.css\n@@ -19,6 +19,10 @@\n width: 100%;\n left: 0px;\n }\n+ .dialog {\n+ width: 100%;\n+ left: 0px;\n+ }\n \n }"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Implemented user registration with username availability check and email field.", "commit": "2ba55f692dfc1b60ac55d514b182fb8834cb99bb", "diff": "commit 2ba55f692dfc1b60ac55d514b182fb8834cb99bb\nAuthor: retoor \nDate: Fri Jan 24 21:19:03 2025 +0100\n\n Finished register.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex f25ffba..0cca574 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -4,6 +4,8 @@ from aiohttp import web\n from app.app import Application as BaseApplication\n from app.cache import time_cache_async\n from jinja_markdown2 import MarkdownExtension\n+from snek.mapper import get_mappers\n+from snek.service import get_services\n from snek.system import http\n from snek.system.middleware import cors_middleware\n from snek.view.about import AboutHTMLView, AboutMDView\n@@ -14,6 +16,7 @@ from snek.view.login_form import LoginFormView\n from snek.view.register import RegisterView\n from snek.view.register_form import RegisterFormView\n from snek.view.web import WebView\n+from types import SimpleNamespace\n \n \n class Application(BaseApplication):\n@@ -29,6 +32,11 @@ class Application(BaseApplication):\n )\n self.jinja2_env.add_extension(MarkdownExtension)\n self.setup_router()\n+ self.setup_services()\n+\n+ def setup_services(self):\n+ self.services = SimpleNamespace(**get_services(app=self))\n+ self.mappers = SimpleNamespace(**get_mappers(app=self))\n \n def setup_router(self):\n self.router.add_get(\"/\", IndexView)\ndiff --git a/src/snek/form/login.py b/src/snek/form/login.py\nindex 910cd73..0d97d41 100644\n--- a/src/snek/form/login.py\n+++ b/src/snek/form/login.py\n@@ -22,4 +22,3 @@ class LoginForm(Form):\n type=\"button\"\n )\n \n-\ndiff --git a/src/snek/form/register.py b/src/snek/form/register.py\nindex 7dff3e4..4252bf1 100644\n--- a/src/snek/form/register.py\n+++ b/src/snek/form/register.py\n@@ -1,10 +1,19 @@\n from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement\n \n+class UsernameField(FormInputElement):\n+\n+ @property\n+ async def errors(self):\n+ result = await super().errors\n+ if self.value and await self.app.services.user.count(username=self.value):\n+ result.append(\"Username is not available.\")\n+ return result\n+\n class RegisterForm(Form):\n \n title = HTMLElement(tag=\"h1\", text=\"Register\")\n \n- username = FormInputElement(\n+ username = UsernameField(\n name=\"username\", \n required=True,\n min_length=2,\ndiff --git a/src/snek/mapper/user.py b/src/snek/mapper/user.py\nindex 5b8671e..642028c 100644\n--- a/src/snek/mapper/user.py\n+++ b/src/snek/mapper/user.py\n@@ -3,4 +3,4 @@ from snek.model.user import UserModel\n \n class UserMapper(BaseMapper):\n table_name = \"user\"\n- model: UserModel \n\\ No newline at end of file\n+ model_class = UserModel \n\\ No newline at end of file\ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nindex cde4b8c..a2b0cb1 100644\n--- a/src/snek/service/user.py\n+++ b/src/snek/service/user.py\n@@ -1,13 +1,14 @@\n from snek.system.service import BaseService \n from snek.system import security \n \n-class UserService:\n+class UserService(BaseService):\n mapper_name = \"user\"\n \n- async def create_user(self, username, password):\n+ async def register(self, email, username, password):\n if await self.exists(username=username):\n raise Exception(\"User already exists.\")\n model = await self.new()\n+ model.email = email\n model.username = username\n model.password = await security.hash(password)\n if await self.save(model):\ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nindex 58a67e2..a47c6db 100644\n--- a/src/snek/static/generic-form.js\n+++ b/src/snek/static/generic-form.js\n@@ -280,7 +280,11 @@ class GenericForm extends HTMLElement {\n if(e.detail.type == \"button\"){\n if(e.detail.value == \"submit\")\n {\n- await me.validate()\n+ const isValid = await me.validate()\n+ if(isValid){\n+ const isProcessed = await me.submit()\n+ console.info({processed:isProcessed})\n+ }\n }\n }\n \n@@ -294,13 +298,15 @@ class GenericForm extends HTMLElement {\n async validate(){\n const url = this.getAttribute(\"url\")\n const me = this\n- const response = await fetch(url,{\n+ let response = await fetch(url,{\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\"action\":\"validate\", \"form\":me.form})\n });\n+\n+ \n const form = await response.json()\n Object.values(form.fields).forEach(field=>{\n if(!me.form.fields[field.name])\n@@ -320,6 +326,22 @@ class GenericForm extends HTMLElement {\n console.info(field.errors)\n me.fields[field.name].setErrors(field.errors)\n })\n+ console.info({XX:form})\n+ return form['is_valid']\n+ }\n+ async submit(){\n+ const me = this \n+ const url = me.getAttribute(\"url\")\n+ const response = await fetch(url,{\n+ method: 'POST',\n+ headers: {\n+ 'Content-Type': 'application/json'\n+ },\n+ body: JSON.stringify({\"action\":\"submit\", \"form\":me.form})\n+ });\n+ return await response.json()\n+ \n }\n+ \n }\n customElements.define('generic-form', GenericForm);\n\\ No newline at end of file\ndiff --git a/src/snek/system/form.py b/src/snek/system/form.py\nindex f9ebebb..82091b6 100644\n--- a/src/snek/system/form.py\n+++ b/src/snek/system/form.py\n@@ -35,8 +35,8 @@ class HTMLElement(model.ModelField):\n self.html = html\n super().__init__(name=name, *args, **kwargs)\n \n- def to_json(self):\n- result = super().to_json()\n+ async def to_json(self):\n+ result = await super().to_json()\n result['text'] = self.text\n result['id'] = self.id\n result['html'] = self.html\n@@ -53,8 +53,8 @@ class FormInputElement(FormElement):\n self.place_holder = place_holder\n self.type = type\n \n- def to_json(self):\n- data = super().to_json()\n+ async def to_json(self):\n+ data = await super().to_json()\n data[\"place_holder\"] = self.place_holder\n data[\"type\"] = self.type\n return data\n@@ -66,31 +66,36 @@ class FormButtonElement(FormElement):\n class Form(model.BaseModel):\n @property\n def html_elements(self):\n- json_elements = super().to_json()\n return [element for element in self.fields if isinstance(element, HTMLElement)]\n \n def set_user_data(self, data):\n return super().set_user_data(data.get('fields'))\n \n- def to_json(self, encode=False):\n- elements = super().to_json()\n+ async def to_json(self, encode=False):\n+ elements = await super().to_json()\n html_elements = {}\n for element in elements.keys():\n+ if element == 'is_valid':\n+ continue \n field = getattr(self, element)\n if isinstance(field, HTMLElement):\n try:\n html_elements[element] = elements[element]\n except KeyError:\n pass\n- return dict(fields=html_elements, is_valid=self.is_valid, errors=self.errors)\n+\n+ is_valid = all(field['is_valid'] for field in html_elements.values())\n+ return dict(fields=html_elements, is_valid=is_valid, errors=await self.errors)\n \n @property\n- def errors(self):\n+ async def errors(self):\n result = []\n for field in self.html_elements:\n- result += field.errors\n+ result += await field.errors\n return result\n \n @property\n- def is_valid(self):\n- return all(element.is_valid for element in self.html_elements)\n\\ No newline at end of file\n+ async def is_valid(self):\n+ return False\n\\ No newline at end of file\ndiff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py\nindex f4beb2e..667aad1 100644\n--- a/src/snek/system/mapper.py\n+++ b/src/snek/system/mapper.py\n@@ -1,25 +1,19 @@\n \n DEFAULT_LIMIT = 30\n+import typing\n from snek.system.model import BaseModel\n-from snek.app import Application \n+\n import types\n \n-class Mapper:\n+class BaseMapper:\n \n model_class:BaseModel = None \n default_limit:int = DEFAULT_LIMIT\n table_name:str = None \n \n- def __init__(self, app:Application, table_name:str, model_class:BaseModel):\n+ def __init__(self, app):\n self.app = app \n \n- if not self.model_class:\n- raise ValueError(\"Mapper configuration error: model_class is not set.\")\n- self.model_class = model_class \n- \n- self.table_name = table_name\n- if not self.table_name:\n- raise ValueError(\"Mapper configuration error: table_name is not set.\")\n self.default_limit = self.__class__.default_limit \n \n @property\n@@ -33,12 +27,12 @@ class Mapper:\n def table(self):\n return self.db[self.table_name]\n \n- async def get(self, uid:str=None, **kwargs) -> types.Optional[BaseModel]\n+ async def get(self, uid:str=None, **kwargs) -> BaseModel:\n if uid:\n kwargs['uid'] = uid \n model = self.new()\n record = self.table.find_one(**kwargs)\n- return self.model_class.from_record(mapper=self,record=record)\n+ return await self.model_class.from_record(mapper=self,record=record)\n \n async def exists(self, **kwargs):\n return self.table.exists(**kwargs)\n@@ -47,16 +41,16 @@ class Mapper:\n return self.table.count(**kwargs)\n \n async def save(self, model:BaseModel) -> bool:\n- record = model.record\n+ record = await model.record\n if not record.get('uid'):\n raise Exception(f\"Attempt to save without uid: {record}.\")\n return self.table.upsert(record,['uid'])\n \n- async def find(self, **kwargs) -> types.List[BaseModel]:\n+ async def find(self, **kwargs) -> typing.AsyncGenerator:\n if not kwargs.get(\"_limit\"):\n kwargs[\"_limit\"] = self.default_limit\n for record in self.table.find(**kwargs):\n- yield self.model_class.from_record(mapper=self,record=record)\n+ yield await self.model_class.from_record(mapper=self,record=record)\n \n async def delete(self, kwargs=None)-> int: \n if not kwargs or not isinstance(kwargs, dict):\ndiff --git a/src/snek/system/model.py b/src/snek/system/model.py\nindex 9a00186..0d700ff 100644\n--- a/src/snek/system/model.py\n+++ b/src/snek/system/model.py\n@@ -70,9 +70,11 @@ class Validator:\n def custom_validation(self):\n return True\n \n- def __init__(self, required=False, min_num=None, max_num=None, min_length=None, max_length=None, regex=None, value=None, kind=None, help_text=None, **kwargs):\n+ def __init__(self, required=False, min_num=None, max_num=None, min_length=None, max_length=None, regex=None, value=None, kind=None, help_text=None, app=None, model=None, **kwargs):\n self.index = Validator._index\n Validator._index += 1\n+ self.app = app\n+ self.model = model\n self.required = required\n self.min_num = min_num\n self.max_num = max_num\n@@ -86,7 +88,7 @@ class Validator:\n self.__dict__.update(kwargs)\n \n @property\n- def errors(self):\n+ async def errors(self):\n error_list = []\n if self.value is None and self.required:\n error_list.append(\"Field is required.\")\n@@ -110,20 +112,23 @@ class Validator:\n error_list.append(f\"Invalid kind. It is supposed to be {self.kind}.\")\n return error_list\n \n- def validate(self):\n- if self.errors:\n- raise ValueError(\"\\n\", self.errors)\n+ async def validate(self):\n+ errors = await self.errors\n+ if errors:\n+ raise ValueError(f\"Errors: {errors}.\")\n return True\n \n @property\n- def is_valid(self):\n+ async def is_valid(self):\n try:\n- self.validate()\n+ await self.validate()\n return True\n except ValueError:\n return False\n \n- def to_json(self):\n+ async def to_json(self):\n+ errors = await self.errors\n+ is_valid = await self.is_valid\n return {\n \"required\": self.required,\n \"min_num\": self.min_num,\n@@ -134,8 +139,8 @@ class Validator:\n \"value\": self.value,\n \"kind\": str(self.kind),\n \"help_text\": self.help_text,\n- \"errors\": self.errors,\n- \"is_valid\": self.is_valid,\n+ \"errors\": errors,\n+ \"is_valid\": is_valid,\n \"index\": self.index\n }\n \n@@ -149,8 +154,8 @@ class ModelField(Validator):\n self.save = save\n super().__init__(*args, **kwargs)\n \n- def to_json(self):\n- result = super().to_json()\n+ async def to_json(self):\n+ result = await super().to_json()\n result['name'] = self.name\n return result\n \n@@ -193,7 +198,7 @@ class BaseModel:\n deleted_at = DeletedField(name=\"deleted_at\", regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\")\n \n @classmethod \n- def from_record(cls, record, mapper):\n+ async def from_record(cls, record, mapper):\n model = cls.__new__()\n model.mapper = mapper \n model.record = record\n@@ -230,6 +235,8 @@ class BaseModel:\n self.__dict__[key] = copy.deepcopy(obj)\n self.__dict__[key].value = kwargs.pop(key, self.__dict__[key].initial_value)\n self.fields[key] = self.__dict__[key]\n+ self.fields[key].model = self\n+ self.fields[key].app = kwargs.get('app')\n \n def __setitem__(self, key, value):\n obj = self.__dict__.get(key)\n@@ -254,11 +261,9 @@ class BaseModel:\n \n \n @property\n- def is_valid(self):\n- for field in self.fields.values():\n- if not field.is_valid:\n- return False\n- return True\n+ async def is_valid(self):\n+ return all([await field.is_valid for field in self.fields.values()])\n+ \n \n def __getitem__(self, key):\n obj = self.__dict__.get(key)\n@@ -273,28 +278,31 @@ class BaseModel:\n self.__dict__[key] = value\n \n @property\n- def record(self):\n- obj = self.to_json()\n+ async def record(self):\n+ obj = await self.to_json()\n record = {}\n for key, value in obj.items():\n+ if not isinstance(value, dict) or not 'value' in value:\n+ continue\n if getattr(self, key).save:\n record[key] = value.get('value')\n return record\n \n- def to_json(self, encode=False):\n+ async def to_json(self, encode=False):\n model_data = OrderedDict({\n \"uid\": self.uid.value,\n \"created_at\": self.created_at.value,\n \"updated_at\": self.updated_at.value,\n- \"deleted_at\": self.deleted_at.value\n+ \"deleted_at\": self.deleted_at.value,\n+ \"is_valid\": await self.is_valid\n })\n \n- for key, value in self.__dict__.items():\n+ for key, value in self.fields.items():\n if key == \"record\":\n continue\n value = self.__dict__[key]\n if hasattr(value, \"value\"):\n- model_data[key] = value.to_json()\n+ model_data[key] = await value.to_json()\n if encode:\n return json.dumps(model_data, indent=2)\n return model_data\n@@ -313,8 +321,8 @@ class FormElement(ModelField):\n self.place_holder = place_holder\n super().__init__(*args, **kwargs)\n \n- def to_json(self):\n- data = super().to_json()\n+ async def to_json(self):\n+ data = await super().to_json()\n data[\"name\"] = self.name\n data[\"place_holder\"] = self.place_holder\n return data\n\\ No newline at end of file\ndiff --git a/src/snek/system/service.py b/src/snek/system/service.py\nindex 5a8b553..d970b5f 100644\n--- a/src/snek/system/service.py\n+++ b/src/snek/system/service.py\n@@ -17,10 +17,10 @@ class BaseService:\n self.mapper = None \n \n async def exists(self, **kwargs):\n- return self.mapper.exists(**kwargs)\n+ return await self.count(**kwargs) > 0\n \n async def count(self, **kwargs):\n- return self.mapper.count(**kwargs)\n+ return await self.mapper.count(**kwargs)\n \n async def new(self, **kwargs):\n return await self.mapper.new()\n@@ -29,9 +29,9 @@ class BaseService:\n return await self.mapper.get(**kwargs)\n \n async def save(self, model:UserModel):\n- if model.is_valid:\n- return self.mapper.save(model) and True \n- return False \n+ return await self.mapper.save(model) and True \n+ \n \n async def find(self, **kwargs):\n return await self.mapper.find(**kwargs)\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nindex 458aa20..3b53f33 100644\n--- a/src/snek/system/view.py\n+++ b/src/snek/system/view.py\n@@ -27,12 +27,21 @@ class BaseFormView(BaseView):\n form = None \n \n async def get(self):\n- form = self.form()\n- return await self.json_response(form.to_json())\n+ form = self.form(app=self.app)\n+ \n+ return await self.json_response(await form.to_json())\n \n async def post(self):\n- form = self.form()\n+ form = self.form(app=self.app)\n post = await self.request.json()\n form.set_user_data(post['form'])\n- return await self.json_response(form.to_json()) \n+ result = await form.to_json()\n+ if post.get('action') == 'validate':\n+ pass\n+ if post.get('action') == 'submit' and result['is_valid']:\n+ await self.submit(form)\n+ return await self.json_response(result) \n \n+ async def submit(self,model=None):\n+ print(\"Submit sucess\")\ndiff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py\nindex 8099b01..8cb6567 100644\n--- a/src/snek/view/register_form.py\n+++ b/src/snek/view/register_form.py\n@@ -2,4 +2,8 @@ from snek.form.register import RegisterForm\n from snek.system.view import BaseFormView\n \n class RegisterFormView(BaseFormView):\n- form = RegisterForm\n\\ No newline at end of file\n+ form = RegisterForm\n+\n+ async def submit(self, form):\n+ result = await self.app.services.user.register(form.email.value,form.username.value,form.password.value)\n+ print(\"SUBMITTED:\",result)\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Added documentation subapp and updated links", "commit": "18b76ebd5e2f11451db04800d426a16b1ef1dd14", "diff": "commit 18b76ebd5e2f11451db04800d426a16b1ef1dd14\nAuthor: retoor \nDate: Fri Jan 24 23:33:36 2025 +0100\n\n Progress.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 0cca574..78c0e2b 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -2,11 +2,12 @@ import pathlib\n \n from aiohttp import web\n from app.app import Application as BaseApplication\n+from snek.docs.app import Application as DocsApplication\n from app.cache import time_cache_async\n-from jinja_markdown2 import MarkdownExtension\n from snek.mapper import get_mappers\n from snek.service import get_services\n from snek.system import http\n+from snek.system.markdown import MarkdownExtension\n from snek.system.middleware import cors_middleware\n from snek.view.about import AboutHTMLView, AboutMDView\n from snek.view.docs import DocsHTMLView, DocsMDView\n@@ -59,6 +60,8 @@ class Application(BaseApplication):\n self.router.add_get(\"/http-get\", self.handle_http_get)\n self.router.add_get(\"/http-photo\", self.handle_http_photo)\n \n+ self.add_subapp(\"/docs\", DocsApplication(path=pathlib.Path(__file__).parent.joinpath(\"docs\")))\n+\n async def handle_test(self, request):\n \n return await self.render_template(\ndiff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py\nindex 489a3e5..fcffe40 100644\n--- a/src/snek/system/markdown.py\n+++ b/src/snek/system/markdown.py\n@@ -1,6 +1,7 @@\n \n \n+from types import SimpleNamespace\n from mistune import escape\n from mistune import Markdown\n from mistune import HTMLRenderer\n@@ -12,6 +13,8 @@ import functools\n from app.cache import time_cache_async\n \n class MarkdownRenderer(HTMLRenderer):\n+\n+ _allow_harmful_protocols = True\n def __init__(self, app, template):\n self.template = template\n \n@@ -45,4 +48,29 @@ def render_markdown_sync(app, markdown_string):\n \n @time_cache_async(120)\n async def render_markdown(app, markdown_string):\n- return render_markdown_sync(app,markdown_string)\n\\ No newline at end of file\n+ return render_markdown_sync(app,markdown_string)\n+\n+from jinja2 import nodes, TemplateSyntaxError\n+from jinja2.ext import Extension\n+from jinja2.nodes import Const\n+\n+class MarkdownExtension(Extension):\n+ tags = {'markdown'}\n+\n+ def __init__(self, environment):\n+ self.app = SimpleNamespace(jinja2_env=environment)\n+ super(MarkdownExtension, self).__init__(environment)\n+\n+ def parse(self, parser):\n+ line_number = next(parser.stream).lineno\n+ md_file = [Const('')]\n+ body = ''\n+ try:\n+ md_file = [parser.parse_expression()]\n+ except TemplateSyntaxError:\n+ body = parser.parse_statements(['name:endmarkdown'], drop_needle=True)\n+ return nodes.CallBlock(self.call_method('_to_html', md_file), [], [], body).set_lineno(line_number)\n+\n+ def _to_html(self, md_file, caller):\n+ return render_markdown_sync(self.app,caller())\n\\ No newline at end of file\ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nindex ad02a6a..c6d57df 100644\n--- a/src/snek/templates/index.html\n+++ b/src/snek/templates/index.html\n@@ -16,7 +16,7 @@\n \n Design choices\n App preview\n- API docs\n+ API docs\n \n \n "} +{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor form validation and rendering for improved consistency", "commit": "9b93403a93ac0b03a57fb5dc10db5c35349c4d6f", "diff": "commit 9b93403a93ac0b03a57fb5dc10db5c35349c4d6f\nAuthor: retoor \nDate: Fri Jan 24 23:35:44 2025 +0100\n\n Formatting.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 78c0e2b..ab19f42 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -1,9 +1,10 @@\n import pathlib\n+from types import SimpleNamespace\n \n from aiohttp import web\n from app.app import Application as BaseApplication\n+\n from snek.docs.app import Application as DocsApplication\n-from app.cache import time_cache_async\n from snek.mapper import get_mappers\n from snek.service import get_services\n from snek.system import http\n@@ -17,7 +18,6 @@ from snek.view.login_form import LoginFormView\n from snek.view.register import RegisterView\n from snek.view.register_form import RegisterFormView\n from snek.view.web import WebView\n-from types import SimpleNamespace\n \n \n class Application(BaseApplication):\n@@ -51,7 +51,7 @@ class Application(BaseApplication):\n self.router.add_view(\"/about.md\", AboutMDView)\n self.router.add_view(\"/docs.html\", DocsHTMLView)\n self.router.add_view(\"/docs.md\", DocsMDView)\n- \n+\n self.router.add_view(\"/web.html\", WebView)\n self.router.add_view(\"/login.html\", LoginView)\n self.router.add_view(\"/login.json\", LoginFormView)\n@@ -60,7 +60,10 @@ class Application(BaseApplication):\n self.router.add_get(\"/http-get\", self.handle_http_get)\n self.router.add_get(\"/http-photo\", self.handle_http_photo)\n \n- self.add_subapp(\"/docs\", DocsApplication(path=pathlib.Path(__file__).parent.joinpath(\"docs\")))\n+ self.add_subapp(\n+ \"/docs\",\n+ DocsApplication(path=pathlib.Path(__file__).parent.joinpath(\"docs\")),\n+ )\n \n async def handle_test(self, request):\n \n@@ -79,9 +82,8 @@ class Application(BaseApplication):\n return web.Response(\n body=path.read_bytes(), headers={\"Content-Type\": \"image/png\"}\n )\n- \n- \n+\n async def render_template(self, template, request, context=None):\n return await super().render_template(template, request, context)\n \ndiff --git a/src/snek/form/login.py b/src/snek/form/login.py\nindex 0d97d41..3d6d9a7 100644\n--- a/src/snek/form/login.py\n+++ b/src/snek/form/login.py\n@@ -1,24 +1,27 @@\n-from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement\n+from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement\n+\n \n class LoginForm(Form):\n \n title = HTMLElement(tag=\"h1\", text=\"Login\")\n \n username = FormInputElement(\n- name=\"username\", \n+ name=\"username\",\n required=True,\n min_length=2,\n max_length=20,\n regex=r\"^[a-zA-Z0-9_]+$\",\n place_holder=\"Username\",\n- type=\"text\"\n+ type=\"text\",\n+ )\n+ password = FormInputElement(\n+ name=\"password\",\n+ required=True,\n+ regex=r\"^[a-zA-Z0-9_.+-]{6,}\",\n+ type=\"password\",\n+ place_holder=\"Password\",\n )\n- password = FormInputElement(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\",type=\"password\",place_holder=\"Password\")\n \n action = FormButtonElement(\n- name=\"action\",\n- value=\"submit\",\n- text=\"Login\",\n- type=\"button\"\n+ name=\"action\", value=\"submit\", text=\"Login\", type=\"button\"\n )\n- \ndiff --git a/src/snek/form/register.py b/src/snek/form/register.py\nindex 4252bf1..1384b8f 100644\n--- a/src/snek/form/register.py\n+++ b/src/snek/form/register.py\n@@ -1,4 +1,5 @@\n-from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement\n+from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement\n+\n \n class UsernameField(FormInputElement):\n \n@@ -9,32 +10,35 @@ class UsernameField(FormInputElement):\n result.append(\"Username is not available.\")\n return result\n \n+\n class RegisterForm(Form):\n \n title = HTMLElement(tag=\"h1\", text=\"Register\")\n \n username = UsernameField(\n- name=\"username\", \n+ name=\"username\",\n required=True,\n min_length=2,\n max_length=20,\n regex=r\"^[a-zA-Z0-9_]+$\",\n place_holder=\"Username\",\n- type=\"text\"\n+ type=\"text\",\n )\n email = FormInputElement(\n name=\"email\",\n required=False,\n regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n place_holder=\"Email address\",\n- type=\"email\"\n+ type=\"email\",\n+ )\n+ password = FormInputElement(\n+ name=\"password\",\n+ required=True,\n+ regex=r\"^[a-zA-Z0-9_.+-]{6,}\",\n+ type=\"password\",\n+ place_holder=\"Password\",\n )\n- password = FormInputElement(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\",type=\"password\",place_holder=\"Password\")\n \n action = FormButtonElement(\n- name=\"action\",\n- value=\"submit\",\n- text=\"Register\",\n- type=\"button\"\n+ name=\"action\", value=\"submit\", text=\"Register\", type=\"button\"\n )\n-\ndiff --git a/src/snek/mapper/__init__.py b/src/snek/mapper/__init__.py\nindex dc9e047..2b9b79f 100644\n--- a/src/snek/mapper/__init__.py\n+++ b/src/snek/mapper/__init__.py\n@@ -1,12 +1,12 @@\n-import functools \n+import functools\n+\n from snek.mapper.user import UserMapper\n \n-@functools.cache \n+\n+@functools.cache\n def get_mappers(app=None):\n- return dict(\n- user=UserMapper(app=app)\n+ return {\"user\": UserMapper(app=app)}\n \n- )\n \n def get_mapper(name, app=None):\n- return get_mappers(app=app)[name]\n\\ No newline at end of file\n+ return get_mappers(app=app)[name]\ndiff --git a/src/snek/mapper/user.py b/src/snek/mapper/user.py\nindex 642028c..c388abc 100644\n--- a/src/snek/mapper/user.py\n+++ b/src/snek/mapper/user.py\n@@ -1,6 +1,7 @@\n-from snek.system.mapper import BaseMapper\n from snek.model.user import UserModel\n+from snek.system.mapper import BaseMapper\n+\n \n class UserMapper(BaseMapper):\n table_name = \"user\"\n- model_class = UserModel \n\\ No newline at end of file\n+ model_class = UserModel\ndiff --git a/src/snek/model/__init__.py b/src/snek/model/__init__.py\nindex 52af21a..081ae15 100644\n--- a/src/snek/model/__init__.py\n+++ b/src/snek/model/__init__.py\n@@ -1,12 +1,12 @@\n-from snek.model.user import UserModel \n-import functools \n+import functools\n+\n+from snek.model.user import UserModel\n+\n \n @functools.cache\n def get_models():\n- return dict(\n- user=UserModel\n+ return {\"user\": UserModel}\n \n- )\n \n def get_model(name):\n return get_models()[name]\ndiff --git a/src/snek/model/user.py b/src/snek/model/user.py\nindex 254b6c9..adb236b 100644\n--- a/src/snek/model/user.py\n+++ b/src/snek/model/user.py\n@@ -1,9 +1,10 @@\n-from snek.system.model import BaseModel,ModelField\n+from snek.system.model import BaseModel, ModelField\n+\n \n class UserModel(BaseModel):\n- \n+\n username = ModelField(\n- name=\"username\", \n+ name=\"username\",\n required=True,\n min_length=2,\n max_length=20,\n@@ -12,8 +13,6 @@ class UserModel(BaseModel):\n email = ModelField(\n name=\"email\",\n required=False,\n- regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\"\n+ regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n )\n- password = ModelField(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\")\n-\n-\n+ password = ModelField(name=\"password\", required=True, regex=r\"^[a-zA-Z0-9_.+-]{6,}\")\ndiff --git a/src/snek/service/__init__.py b/src/snek/service/__init__.py\nindex 4038f70..60fec76 100644\n--- a/src/snek/service/__init__.py\n+++ b/src/snek/service/__init__.py\n@@ -1,12 +1,13 @@\n-from snek.service.user import UserService \n-import functools \n+import functools\n+\n+from snek.service.user import UserService\n+\n \n @functools.cache\n def get_services(app):\n \n- return dict(\n- user = UserService(app=app)\n+ return {\"user\": UserService(app=app)}\n+\n \n- )\n def get_service(name, app=None):\n- return get_services(app=app)[name]\n\\ No newline at end of file\n+ return get_services(app=app)[name]\ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nindex a2b0cb1..5124640 100644\n--- a/src/snek/service/user.py\n+++ b/src/snek/service/user.py\n@@ -1,5 +1,6 @@\n-from snek.system.service import BaseService \n-from snek.system import security \n+from snek.system import security\n+from snek.system.service import BaseService\n+\n \n class UserService(BaseService):\n mapper_name = \"user\"\n@@ -12,6 +13,5 @@ class UserService(BaseService):\n model.username = username\n model.password = await security.hash(password)\n if await self.save(model):\n- return model \n+ return model\n raise Exception(f\"Failed to create user: {model.errors}.\")\n- \n\\ No newline at end of file\ndiff --git a/src/snek/system/cache.py b/src/snek/system/cache.py\nindex 2992803..5e275d9 100644\n--- a/src/snek/system/cache.py\n+++ b/src/snek/system/cache.py\n@@ -1,8 +1,8 @@\n-\n-import functools \n+import functools\n \n cache = functools.cache\n \n+\n def async_cache(func):\n cache = {}\n \n@@ -14,4 +14,4 @@ def async_cache(func):\n cache[args] = result\n return result\n \n- return wrapper\n\\ No newline at end of file\n+ return wrapper\ndiff --git a/src/snek/system/form.py b/src/snek/system/form.py\nindex 82091b6..f4cf2d3 100644\n--- a/src/snek/system/form.py\n+++ b/src/snek/system/form.py\n@@ -5,17 +5,17 @@\n \n@@ -26,8 +26,19 @@\n \n from snek.system import model\n \n+\n class HTMLElement(model.ModelField):\n- def __init__(self, id=None, tag=\"div\", name=None, html=None, class_name=None, text=None, *args, **kwargs):\n+ def __init__(\n+ self,\n+ id=None,\n+ tag=\"div\",\n+ name=None,\n+ html=None,\n+ class_name=None,\n+ text=None,\n+ *args,\n+ **kwargs,\n+ ):\n self.tag = tag\n self.text = text\n self.id = id\n@@ -37,16 +48,18 @@ class HTMLElement(model.ModelField):\n \n async def to_json(self):\n result = await super().to_json()\n- result['text'] = self.text\n- result['id'] = self.id\n- result['html'] = self.html\n- result['class_name'] = self.class_name\n- result['tag'] = self.tag\n+ result[\"text\"] = self.text\n+ result[\"id\"] = self.id\n+ result[\"html\"] = self.html\n+ result[\"class_name\"] = self.class_name\n+ result[\"tag\"] = self.tag\n return result\n \n+\n class FormElement(HTMLElement):\n pass\n \n+\n class FormInputElement(FormElement):\n def __init__(self, type=\"text\", place_holder=None, *args, **kwargs):\n super().__init__(tag=\"input\", *args, **kwargs)\n@@ -59,25 +72,27 @@ class FormInputElement(FormElement):\n data[\"type\"] = self.type\n return data\n \n+\n class FormButtonElement(FormElement):\n def __init__(self, tag=\"button\", *args, **kwargs):\n super().__init__(tag=tag, *args, **kwargs)\n \n+\n class Form(model.BaseModel):\n @property\n def html_elements(self):\n return [element for element in self.fields if isinstance(element, HTMLElement)]\n \n def set_user_data(self, data):\n- return super().set_user_data(data.get('fields'))\n+ return super().set_user_data(data.get(\"fields\"))\n \n async def to_json(self, encode=False):\n elements = await super().to_json()\n html_elements = {}\n for element in elements.keys():\n- if element == 'is_valid':\n+ if element == \"is_valid\":\n- continue \n+ continue\n field = getattr(self, element)\n if isinstance(field, HTMLElement):\n try:\n@@ -85,8 +100,12 @@ class Form(model.BaseModel):\n except KeyError:\n pass\n \n- is_valid = all(field['is_valid'] for field in html_elements.values())\n- return dict(fields=html_elements, is_valid=is_valid, errors=await self.errors)\n+ is_valid = all(field[\"is_valid\"] for field in html_elements.values())\n+ return {\n+ \"fields\": html_elements,\n+ \"is_valid\": is_valid,\n+ \"errors\": await self.errors,\n+ }\n \n @property\n async def errors(self):\n@@ -98,4 +117,4 @@ class Form(model.BaseModel):\n @property\n async def is_valid(self):\n- return False\n\\ No newline at end of file\n+ return False\ndiff --git a/src/snek/system/http.py b/src/snek/system/http.py\nindex b5e8b4f..cd8a9b1 100644\n--- a/src/snek/system/http.py\n+++ b/src/snek/system/http.py\n@@ -11,10 +11,10 @@\n@@ -24,17 +24,17 @@\n \n \n-from aiohttp import web\n-import aiohttp\n-from app.cache import time_cache_async\n-from bs4 import BeautifulSoup\n-from urllib.parse import urljoin\n+import asyncio\n import pathlib\n import uuid\n-import imgkit\n-import asyncio\n import zlib\n-import io\n+from urllib.parse import urljoin\n+\n+import aiohttp\n+import imgkit\n+from app.cache import time_cache_async\n+from bs4 import BeautifulSoup\n+\n \n async def crc32(data):\n try:\n@@ -43,6 +43,7 @@ async def crc32(data):\n pass\n return \"crc32\" + str(zlib.crc32(data))\n \n+\n async def get_file(name, suffix=\".cache\"):\n name = await crc32(name)\n path = pathlib.Path(\".\").joinpath(\"cache\")\n@@ -50,17 +51,19 @@ async def get_file(name, suffix=\".cache\"):\n path.mkdir(parents=True, exist_ok=True)\n return path.joinpath(name + suffix)\n \n+\n async def public_touch(name=None):\n path = pathlib.Path(\".\").joinpath(str(uuid.uuid4()) + name)\n path.open(\"wb\").close()\n return path\n \n+\n async def create_site_photo(url):\n loop = asyncio.get_event_loop()\n if not url.startswith(\"https\"):\n output_path = await get_file(\"site-screenshot-\" + url, \".png\")\n- \n+\n if output_path.exists():\n return output_path\n output_path.touch()\n@@ -71,21 +74,23 @@ async def create_site_photo(url):\n \n return await loop.run_in_executor(None, make_photo)\n \n+\n async def repair_links(base_url, html_content):\n soup = BeautifulSoup(html_content, \"html.parser\")\n- for tag in soup.find_all(['a', 'img', 'link']):\n- if tag.has_attr('href') and not tag['href'].startswith(\"http\"):\n- tag['href'] = urljoin(base_url, tag['href'])\n- if tag.has_attr('src') and not tag['src'].startswith(\"http\"):\n- tag['src'] = urljoin(base_url, tag['src'])\n+ for tag in soup.find_all([\"a\", \"img\", \"link\"]):\n+ if tag.has_attr(\"href\") and not tag[\"href\"].startswith(\"http\"):\n+ tag[\"href\"] = urljoin(base_url, tag[\"href\"])\n+ if tag.has_attr(\"src\") and not tag[\"src\"].startswith(\"http\"):\n+ tag[\"src\"] = urljoin(base_url, tag[\"src\"])\n return soup.prettify()\n \n+\n async def is_html_content(content: bytes):\n try:\n- content = content.decode(errors='ignore')\n+ content = content.decode(errors=\"ignore\")\n except:\n pass\n- marks = [' BaseModel:\n+ async def get(self, uid: str = None, **kwargs) -> BaseModel:\n if uid:\n- kwargs['uid'] = uid \n- model = self.new()\n+ kwargs[\"uid\"] = uid\n+ self.new()\n record = self.table.find_one(**kwargs)\n- return await self.model_class.from_record(mapper=self,record=record)\n+ return await self.model_class.from_record(mapper=self, record=record)\n \n async def exists(self, **kwargs):\n return self.table.exists(**kwargs)\n@@ -40,19 +39,19 @@ class BaseMapper:\n async def count(self, **kwargs) -> int:\n return self.table.count(**kwargs)\n \n- async def save(self, model:BaseModel) -> bool:\n+ async def save(self, model: BaseModel) -> bool:\n record = await model.record\n- if not record.get('uid'):\n+ if not record.get(\"uid\"):\n raise Exception(f\"Attempt to save without uid: {record}.\")\n- return self.table.upsert(record,['uid'])\n+ return self.table.upsert(record, [\"uid\"])\n \n async def find(self, **kwargs) -> typing.AsyncGenerator:\n if not kwargs.get(\"_limit\"):\n kwargs[\"_limit\"] = self.default_limit\n for record in self.table.find(**kwargs):\n- yield await self.model_class.from_record(mapper=self,record=record)\n- \n- async def delete(self, kwargs=None)-> int: \n+ yield await self.model_class.from_record(mapper=self, record=record)\n+\n+ async def delete(self, kwargs=None) -> int:\n if not kwargs or not isinstance(kwargs, dict):\n raise Exception(\"Can't execute delete with no filter.\")\n return self.table.delete(**kwargs)\ndiff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py\nindex fcffe40..23d0656 100644\n--- a/src/snek/system/markdown.py\n+++ b/src/snek/system/markdown.py\n@@ -1,62 +1,65 @@\n-\n \n from types import SimpleNamespace\n-from mistune import escape\n-from mistune import Markdown\n-from mistune import HTMLRenderer\n+\n+from app.cache import time_cache_async\n+from mistune import HTMLRenderer, Markdown\n from pygments import highlight\n-from pygments.lexers import get_lexer_by_name\n from pygments.formatters import html\n-from pygments.styles import get_style_by_name\n-import functools\n-from app.cache import time_cache_async\n+from pygments.lexers import get_lexer_by_name\n+\n \n class MarkdownRenderer(HTMLRenderer):\n \n _allow_harmful_protocols = True\n+\n def __init__(self, app, template):\n- self.template = template\n- \n- self.app = app\n- self.env = self.app.jinja2_env\n- formatter = html.HtmlFormatter()\n- self.env.globals['highlight_styles'] = formatter.get_style_defs()\n- def _escape(self,str):\n- def block_code(self, code, lang=None,info=None):\n+ self.template = template\n+\n+ self.app = app\n+ self.env = self.app.jinja2_env\n+ formatter = html.HtmlFormatter()\n+ self.env.globals[\"highlight_styles\"] = formatter.get_style_defs()\n+\n+ def _escape(self, str):\n+\n+ def block_code(self, code, lang=None, info=None):\n if not lang:\n lang = info\n if not lang:\n return f\"
{code}
\"\n lexer = get_lexer_by_name(lang, stripall=True)\n formatter = html.HtmlFormatter(lineseparator=\"
\")\n return highlight(code, lexer, formatter)\n+\n def render(self):\n markdown_string = self.app.template_path.joinpath(self.template).read_text()\n- renderer = MarkdownRenderer(self.app,self.template)\n+ renderer = MarkdownRenderer(self.app, self.template)\n markdown = Markdown(renderer=renderer)\n return markdown(markdown_string)\n \n \n-\n def render_markdown_sync(app, markdown_string):\n- renderer = MarkdownRenderer(app,None)\n+ renderer = MarkdownRenderer(app, None)\n markdown = Markdown(renderer=renderer)\n return markdown(markdown_string)\n \n+\n @time_cache_async(120)\n async def render_markdown(app, markdown_string):\n- return render_markdown_sync(app,markdown_string)\n+ return render_markdown_sync(app, markdown_string)\n \n-from jinja2 import nodes, TemplateSyntaxError\n+\n+from jinja2 import TemplateSyntaxError, nodes\n from jinja2.ext import Extension\n from jinja2.nodes import Const\n \n+\n class MarkdownExtension(Extension):\n- tags = {'markdown'}\n+ tags = {\"markdown\"}\n \n def __init__(self, environment):\n self.app = SimpleNamespace(jinja2_env=environment)\n@@ -64,13 +67,15 @@ class MarkdownExtension(Extension):\n \n def parse(self, parser):\n line_number = next(parser.stream).lineno\n- md_file = [Const('')]\n- body = ''\n+ md_file = [Const(\"\")]\n+ body = \"\"\n try:\n md_file = [parser.parse_expression()]\n except TemplateSyntaxError:\n- body = parser.parse_statements(['name:endmarkdown'], drop_needle=True)\n- return nodes.CallBlock(self.call_method('_to_html', md_file), [], [], body).set_lineno(line_number)\n+ body = parser.parse_statements([\"name:endmarkdown\"], drop_needle=True)\n+ return nodes.CallBlock(\n+ self.call_method(\"_to_html\", md_file), [], [], body\n+ ).set_lineno(line_number)\n \n def _to_html(self, md_file, caller):\n- return render_markdown_sync(self.app,caller())\n\\ No newline at end of file\n+ return render_markdown_sync(self.app, caller())\ndiff --git a/src/snek/system/middleware.py b/src/snek/system/middleware.py\nindex 7fe457f..69fe378 100644\n--- a/src/snek/system/middleware.py\n+++ b/src/snek/system/middleware.py\n@@ -8,12 +8,14 @@\n \n from aiohttp import web\n \n+\n @web.middleware\n async def no_cors_middleware(request, handler):\n response = await handler(request)\n response.headers.pop(\"Access-Control-Allow-Origin\", None)\n return response\n \n+\n @web.middleware\n async def cors_allow_middleware(request, handler):\n response = await handler(request)\n@@ -22,12 +24,15 @@ async def cors_allow_middleware(request, handler):\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n return response\n \n+\n @web.middleware\n async def cors_middleware(request, handler):\n if request.method == \"OPTIONS\":\n response = web.Response()\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n- response.headers[\"Access-Control-Allow-Methods\"] = \"GET, POST, PUT, DELETE, OPTIONS\"\n+ response.headers[\"Access-Control-Allow-Methods\"] = (\n+ \"GET, POST, PUT, DELETE, OPTIONS\"\n+ )\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n return response\n \n@@ -35,4 +40,4 @@ async def cors_middleware(request, handler):\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n response.headers[\"Access-Control-Allow-Methods\"] = \"GET, POST, PUT, DELETE, OPTIONS\"\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n- return response\n\\ No newline at end of file\n+ return response\ndiff --git a/src/snek/system/model.py b/src/snek/system/model.py\nindex 0d700ff..b41f4ba 100644\n--- a/src/snek/system/model.py\n+++ b/src/snek/system/model.py\n@@ -25,12 +25,12 @@\n \n \n+import copy\n+import json\n import re\n import uuid\n-import json\n-from datetime import datetime, timezone\n from collections import OrderedDict\n-import copy\n+from datetime import datetime, timezone\n \n TIMESTAMP_REGEX = r\"^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}\\+\\d{2}:\\d{2}$\"\n \n@@ -44,12 +44,21 @@ def add_attrs(**kwargs):\n for key, value in kwargs.items():\n setattr(func, key, value)\n return func\n+\n return decorator\n \n \n-def validate_attrs(required=False, min_length=None, max_length=None, regex=None, **kwargs):\n+def validate_attrs(\n+ required=False, min_length=None, max_length=None, regex=None, **kwargs\n+):\n def decorator(func):\n- return add_attrs(required=required, min_length=min_length, max_length=max_length, regex=regex, **kwargs)(func)\n+ return add_attrs(\n+ required=required,\n+ min_length=min_length,\n+ max_length=max_length,\n+ regex=regex,\n+ **kwargs,\n+ )(func)\n \n \n class Validator:\n@@ -70,7 +79,21 @@ class Validator:\n def custom_validation(self):\n return True\n \n- def __init__(self, required=False, min_num=None, max_num=None, min_length=None, max_length=None, regex=None, value=None, kind=None, help_text=None, app=None, model=None, **kwargs):\n+ def __init__(\n+ self,\n+ required=False,\n+ min_num=None,\n+ max_num=None,\n+ min_length=None,\n+ max_length=None,\n+ regex=None,\n+ value=None,\n+ kind=None,\n+ help_text=None,\n+ app=None,\n+ model=None,\n+ **kwargs,\n+ ):\n self.index = Validator._index\n Validator._index += 1\n self.app = app\n@@ -103,9 +126,13 @@ class Validator:\n if self.max_num is not None and self.value > self.max_num:\n error_list.append(f\"Field should be maximal {self.max_num}.\")\n if self.min_length is not None and len(self.value) < self.min_length:\n- error_list.append(f\"Field should be minimal {self.min_length} characters long.\")\n+ error_list.append(\n+ f\"Field should be minimal {self.min_length} characters long.\"\n+ )\n if self.max_length is not None and len(self.value) > self.max_length:\n- error_list.append(f\"Field should be maximal {self.max_length} characters long.\")\n+ error_list.append(\n+ f\"Field should be maximal {self.max_length} characters long.\"\n+ )\n if self.regex and self.value and not re.match(self.regex, self.value):\n error_list.append(\"Invalid value.\")\n if self.kind and not isinstance(self.value, self.kind):\n@@ -141,7 +168,7 @@ class Validator:\n \"help_text\": self.help_text,\n \"errors\": errors,\n \"is_valid\": is_valid,\n- \"index\": self.index\n+ \"index\": self.index,\n }\n \n \n@@ -156,7 +183,7 @@ class ModelField(Validator):\n \n async def to_json(self):\n result = await super().to_json()\n- result['name'] = self.name\n+ result[\"name\"] = self.name\n return result\n \n \n@@ -193,30 +220,39 @@ class UUIDField(ModelField):\n class BaseModel:\n \n uid = UUIDField(name=\"uid\", required=True)\n- created_at = CreatedField(name=\"created_at\", required=True, regex=TIMESTAMP_REGEX, place_holder=\"Created at\")\n- updated_at = UpdatedField(name=\"updated_at\", regex=TIMESTAMP_REGEX, place_holder=\"Updated at\")\n- deleted_at = DeletedField(name=\"deleted_at\", regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\")\n-\n- @classmethod \n+ created_at = CreatedField(\n+ name=\"created_at\",\n+ required=True,\n+ regex=TIMESTAMP_REGEX,\n+ place_holder=\"Created at\",\n+ )\n+ updated_at = UpdatedField(\n+ name=\"updated_at\", regex=TIMESTAMP_REGEX, place_holder=\"Updated at\"\n+ )\n+ deleted_at = DeletedField(\n+ name=\"deleted_at\", regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\"\n+ )\n+\n+ @classmethod\n async def from_record(cls, record, mapper):\n model = cls.__new__()\n- model.mapper = mapper \n+ model.mapper = mapper\n model.record = record\n return model\n \n- @property \n+ @property\n def mapper(self):\n- return self._mapper \n+ return self._mapper\n \n- @mapper.setter \n+ @mapper.setter\n def mapper(self, value):\n- self._mapper = value \n+ self._mapper = value\n \n- @property \n+ @property\n def record(self):\n return {field.name: field.value for field in self.fields}\n- \n- @record.setter \n+\n+ @record.setter\n def record(self, value):\n for key, value in self._record.items():\n field = self.fields.get(key)\n@@ -233,10 +269,12 @@ class BaseModel:\n \n if isinstance(obj, Validator):\n self.__dict__[key] = copy.deepcopy(obj)\n- self.__dict__[key].value = kwargs.pop(key, self.__dict__[key].initial_value)\n+ self.__dict__[key].value = kwargs.pop(\n+ key, self.__dict__[key].initial_value\n+ )\n self.fields[key] = self.__dict__[key]\n self.fields[key].model = self\n- self.fields[key].app = kwargs.get('app')\n+ self.fields[key].app = kwargs.get(\"app\")\n \n def __setitem__(self, key, value):\n obj = self.__dict__.get(key)\n@@ -254,16 +292,13 @@ class BaseModel:\n field = self.fields.get(key)\n if not field:\n continue\n- if value.get('name'):\n- value = value.get('value')\n+ if value.get(\"name\"):\n+ value = value.get(\"value\")\n field.value = value\n \n- \n-\n @property\n async def is_valid(self):\n return all([await field.is_valid for field in self.fields.values()])\n- \n \n def __getitem__(self, key):\n obj = self.__dict__.get(key)\n@@ -282,20 +317,22 @@ class BaseModel:\n obj = await self.to_json()\n record = {}\n for key, value in obj.items():\n- if not isinstance(value, dict) or not 'value' in value:\n+ if not isinstance(value, dict) or \"value\" not in value:\n continue\n if getattr(self, key).save:\n- record[key] = value.get('value')\n+ record[key] = value.get(\"value\")\n return record\n \n async def to_json(self, encode=False):\n- model_data = OrderedDict({\n- \"uid\": self.uid.value,\n- \"created_at\": self.created_at.value,\n- \"updated_at\": self.updated_at.value,\n- \"deleted_at\": self.deleted_at.value,\n- \"is_valid\": await self.is_valid\n- })\n+ model_data = OrderedDict(\n+ {\n+ \"uid\": self.uid.value,\n+ \"created_at\": self.created_at.value,\n+ \"updated_at\": self.updated_at.value,\n+ \"deleted_at\": self.deleted_at.value,\n+ \"is_valid\": await self.is_valid,\n+ }\n+ )\n \n for key, value in self.fields.items():\n if key == \"record\":\n@@ -325,4 +362,4 @@ class FormElement(ModelField):\n data = await super().to_json()\n data[\"name\"] = self.name\n data[\"place_holder\"] = self.place_holder\n- return data\n\\ No newline at end of file\n+ return data\ndiff --git a/src/snek/system/security.py b/src/snek/system/security.py\nindex b319f54..5449c50 100644\n--- a/src/snek/system/security.py\n+++ b/src/snek/system/security.py\n@@ -1,12 +1,13 @@\n-import hashlib \n+import hashlib\n \n DEFAULT_SALT = b\"snekker-de-snek-\"\n \n-async def hash(data,salt=DEFAULT_SALT):\n+\n+async def hash(data, salt=DEFAULT_SALT):\n try:\n data = data.encode(errors=\"ignore\")\n except AttributeError:\n- pass \n+ pass\n try:\n salt = salt.encode(errors=\"ignore\")\n except AttributeError:\n@@ -16,5 +17,6 @@ async def hash(data,salt=DEFAULT_SALT):\n obj = hashlib.sha256(salted)\n return obj.hexdigest()\n \n-async def verify(string:str, hashed:str):\n- return await hash(string) == hashed \n+\n+async def verify(string: str, hashed: str):\n+ return await hash(string) == hashed\ndiff --git a/src/snek/system/service.py b/src/snek/system/service.py\nindex d970b5f..1f9d601 100644\n--- a/src/snek/system/service.py\n+++ b/src/snek/system/service.py\n@@ -1,24 +1,22 @@\n-\n-\n-\n from snek.mapper import get_mapper\n-from snek.system.mapper import BaseMapper \n from snek.model.user import UserModel\n+from snek.system.mapper import BaseMapper\n+\n \n class BaseService:\n \n- mapper_name:BaseMapper = None\n+ mapper_name: BaseMapper = None\n \n def __init__(self, app):\n- self.app = app \n+ self.app = app\n if self.mapper_name:\n self.mapper = get_mapper(self.mapper_name, app=self.app)\n else:\n- self.mapper = None \n+ self.mapper = None\n \n async def exists(self, **kwargs):\n return await self.count(**kwargs) > 0\n- \n+\n async def count(self, **kwargs):\n return await self.mapper.count(**kwargs)\n \n@@ -27,14 +25,13 @@ class BaseService:\n \n async def get(self, **kwargs):\n return await self.mapper.get(**kwargs)\n- \n- async def save(self, model:UserModel):\n+\n+ async def save(self, model: UserModel):\n- return await self.mapper.save(model) and True \n- \n- \n+ return await self.mapper.save(model) and True\n+\n async def find(self, **kwargs):\n return await self.mapper.find(**kwargs)\n- \n+\n async def delete(self, **kwargs):\n- return await self.mapper.delete(**kwargs)\n\\ No newline at end of file\n+ return await self.mapper.delete(**kwargs)\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nindex 3b53f33..1cf5329 100644\n--- a/src/snek/system/view.py\n+++ b/src/snek/system/view.py\n@@ -1,13 +1,14 @@\n from aiohttp import web\n \n-from snek.system.markdown import render_markdown \n+from snek.system.markdown import render_markdown\n+\n \n class BaseView(web.View):\n- \n- @property \n+\n+ @property\n def app(self):\n return self.request.app\n- \n+\n @property\n def db(self):\n return self.app.db\n@@ -17,31 +18,36 @@ class BaseView(web.View):\n \n async def render_template(self, template_name, context=None):\n if template_name.endswith(\".md\"):\n- response = await self.request.app.render_template(template_name,self.request,context)\n+ response = await self.request.app.render_template(\n+ template_name, self.request, context\n+ )\n body = await render_markdown(self.app, response.body.decode())\n- return web.Response(body=body,content_type=\"text/html\")\n- return await self.request.app.render_template(template_name, self.request,context)\n- \n+ return web.Response(body=body, content_type=\"text/html\")\n+ return await self.request.app.render_template(\n+ template_name, self.request, context\n+ )\n+\n+\n class BaseFormView(BaseView):\n \n- form = None \n+ form = None\n \n async def get(self):\n form = self.form(app=self.app)\n- \n+\n return await self.json_response(await form.to_json())\n- \n+\n async def post(self):\n form = self.form(app=self.app)\n post = await self.request.json()\n- form.set_user_data(post['form'])\n+ form.set_user_data(post[\"form\"])\n result = await form.to_json()\n- if post.get('action') == 'validate':\n+ if post.get(\"action\") == \"validate\":\n pass\n- if post.get('action') == 'submit' and result['is_valid']:\n+ if post.get(\"action\") == \"submit\" and result[\"is_valid\"]:\n await self.submit(form)\n- return await self.json_response(result) \n+ return await self.json_response(result)\n \n- async def submit(self,model=None):\n+ async def submit(self, model=None):\n print(\"Submit sucess\")\ndiff --git a/src/snek/templates/docs.md b/src/snek/templates/docs.md\nindex 2cf60cb..a42b7d5 100644\n--- a/src/snek/templates/docs.md\n+++ b/src/snek/templates/docs.md\n@@ -9,8 +9,7 @@ Currently only some details about the internal API are available.\n \n new_user_object = await app.service.user.register(\n- username=\"retoor\", \n- password=\"retoorded\"\n+ username=\"retoor\", password=\"retoorded\"\n )\n ```\n \n@@ -23,15 +22,16 @@ var1 = security.encrypt(\"data\")\n var2 = security.encrypt(b\"data\")\n \n-assert(var1 == var2)\n+assert var1 == var2\n ```\n \n ```python\n-from snek.system.view import BaseView \n+from snek.system.view import BaseView\n+\n \n class IndexView(BaseView):\n- \n+\n async def get(self):\n@@ -40,11 +40,12 @@ class IndexView(BaseView):\n ```\n ```python\n-from snek.system.view import BaseFormView\n from snek.form.register import RegisterForm\n+from snek.system.view import BaseFormView\n+\n \n class RegisterFormView(BaseFormView):\n- \n+\n form = RegisterForm\n ```\ndiff --git a/src/snek/view/about.py b/src/snek/view/about.py\nindex 593d5a9..762fc8e 100644\n--- a/src/snek/view/about.py\n+++ b/src/snek/view/about.py\n@@ -1,5 +1,3 @@\n-\n-\n from snek.system.view import BaseView\n \n \n@@ -7,8 +5,9 @@ class AboutHTMLView(BaseView):\n \n async def get(self):\n return await self.render_template(\"about.html\")\n- \n+\n+\n class AboutMDView(BaseView):\n \n async def get(self):\n- return await self.render_template(\"about.md\")\n\\ No newline at end of file\n+ return await self.render_template(\"about.md\")\ndiff --git a/src/snek/view/docs.py b/src/snek/view/docs.py\nindex e69d754..519a0eb 100644\n--- a/src/snek/view/docs.py\n+++ b/src/snek/view/docs.py\n@@ -1,6 +1,3 @@\n-\n-\n-\n from snek.system.view import BaseView\n \n \n@@ -8,8 +5,9 @@ class DocsHTMLView(BaseView):\n \n async def get(self):\n return await self.render_template(\"docs.html\")\n- \n+\n+\n class DocsMDView(BaseView):\n \n async def get(self):\n- return await self.render_template(\"docs.md\")\n\\ No newline at end of file\n+ return await self.render_template(\"docs.md\")\ndiff --git a/src/snek/view/index.py b/src/snek/view/index.py\nindex c7861fa..bd91dc8 100644\n--- a/src/snek/view/index.py\n+++ b/src/snek/view/index.py\n@@ -1,5 +1,6 @@\n from snek.system.view import BaseView\n \n+\n class IndexView(BaseView):\n \n async def get(self):\ndiff --git a/src/snek/view/login.py b/src/snek/view/login.py\nindex ffedc79..6566df9 100644\n--- a/src/snek/view/login.py\n+++ b/src/snek/view/login.py\n@@ -1,13 +1,18 @@\n from snek.form.register import RegisterForm\n from snek.system.view import BaseView\n \n+\n class LoginView(BaseView):\n \n async def get(self):\n- \n+ return await self.render_template(\n+ \"login.html\"\n+\n async def post(self):\n form = RegisterForm()\n form.set_user_data(await self.request.post())\n print(form.is_valid())\n+ return await self.render_template(\n+ \"login.html\", self.request\ndiff --git a/src/snek/view/login_form.py b/src/snek/view/login_form.py\nindex e9b6eac..576ddc6 100644\n--- a/src/snek/view/login_form.py\n+++ b/src/snek/view/login_form.py\n@@ -1,5 +1,6 @@\n-from snek.system.view import BaseFormView\n from snek.form.login import LoginForm\n+from snek.system.view import BaseFormView\n+\n \n class LoginFormView(BaseFormView):\n- form = LoginForm\n\\ No newline at end of file\n+ form = LoginForm\ndiff --git a/src/snek/view/register.py b/src/snek/view/register.py\nindex e3b3038..1186959 100644\n--- a/src/snek/view/register.py\n+++ b/src/snek/view/register.py\n@@ -1,6 +1,7 @@\n from snek.system.view import BaseView\n \n+\n class RegisterView(BaseView):\n \n async def get(self):\n- return await self.render_template(\"register.html\") \n\\ No newline at end of file\n+ return await self.render_template(\"register.html\")\ndiff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py\nindex 8cb6567..4c30169 100644\n--- a/src/snek/view/register_form.py\n+++ b/src/snek/view/register_form.py\n@@ -1,9 +1,12 @@\n from snek.form.register import RegisterForm\n from snek.system.view import BaseFormView\n \n+\n class RegisterFormView(BaseFormView):\n form = RegisterForm\n \n async def submit(self, form):\n- result = await self.app.services.user.register(form.email.value,form.username.value,form.password.value)\n- print(\"SUBMITTED:\",result)\n\\ No newline at end of file\n+ result = await self.app.services.user.register(\n+ form.email.value, form.username.value, form.password.value\n+ )\n+ print(\"SUBMITTED:\", result)\ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex b06563a..d42fcec 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -1,6 +1,7 @@\n from snek.system.view import BaseView\n \n+\n class WebView(BaseView):\n \n async def get(self):\n- return await self.render_template(\"web.html\")\n\\ No newline at end of file\n+ return await self.render_template(\"web.html\")"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Added initial documentation structure and content", "commit": "b56371994f5d3f5c1aa5d63c28efd18856ea8e9b", "diff": "commit b56371994f5d3f5c1aa5d63c28efd18856ea8e9b\nAuthor: retoor \nDate: Fri Jan 24 23:41:54 2025 +0100\n\n Added docs.\n\ndiff --git a/src/snek/docs/__pycache__/app.cpython-312.pyc b/src/snek/docs/__pycache__/app.cpython-312.pyc\nnew file mode 100644\nindex 0000000..087bb64\nBinary files /dev/null and b/src/snek/docs/__pycache__/app.cpython-312.pyc differ\ndiff --git a/src/snek/docs/app.py b/src/snek/docs/app.py\nnew file mode 100644\nindex 0000000..5e00d98\n--- /dev/null\n+++ b/src/snek/docs/app.py\n@@ -0,0 +1,33 @@\n+from app.app import Application as BaseApplication\n+import pathlib \n+from aiohttp import web\n+from snek.system.markdown import MarkdownExtension\n+\n+from snek.system.markdown import render_markdown \n+\n+class Application(BaseApplication):\n+\n+ def __init__(self, path=None, *args,**kwargs):\n+ self.path = pathlib.Path(path)\n+ template_path = self.path\n+\n+ super().__init__(template_path=template_path ,*args, **kwargs)\n+ self.jinja2_env.add_extension(MarkdownExtension) \n+ \n+ self.router.add_get(\"/{tail:.*}\",self.handle_document)\n+\n+ async def handle_document(self, request):\n+ relative_path = request.match_info['tail'].strip(\"/\")\n+ if relative_path == '':\n+ relative_path = 'index.html'\n+ document_path = self.path.joinpath(relative_path)\n+ if not document_path.exists():\n+ return web.Response(status=404,body=b'Resource is not found on this server.',content_type=\"text/plain\")\n+ if document_path.is_dir():\n+ document_path = document_path.joinpath(\"index.html\")\n+ if not document_path.exists():\n+ return web.Response(status=404,body=b'Resource is not found on this server.',content_type=\"text/plain\")\n+ \n+ response = await self.render_template(str(document_path.relative_to(self.path)),request)\n+ return response\n+\ndiff --git a/src/snek/docs/docs/api.html b/src/snek/docs/docs/api.html\nnew file mode 100644\nindex 0000000..e30a99d\n--- /dev/null\n+++ b/src/snek/docs/docs/api.html\n@@ -0,0 +1,61 @@\n+{% extends \"docs/base.html\" %}\n+\n+{% block main %}\n+{% markdown %}\n+\n+\n+Currently only some details about the internal API are available.\n+\n+```python\n+\n+new_user_object = await app.service.user.register(\n+ username=\"retoor\", \n+ password=\"retoorded\"\n+)\n+```\n+\n+```python\n+from snek.system import security\n+\n+var1 = security.encrypt(\"data\")\n+var2 = security.encrypt(b\"data\")\n+\n+assert(var1 == var2)\n+```\n+\n+```python\n+from snek.system.view import BaseView \n+\n+class IndexView(BaseView):\n+ \n+ async def get(self):\n+ return await self.render(\"index.html\")\n+```\n+```python\n+from snek.system.view import BaseFormView\n+from snek.form.register import RegisterForm\n+\n+class RegisterFormView(BaseFormView):\n+ \n+ form = RegisterForm\n+```\n+```python\n+app.routes.add_view(\"/your-page.html\", YourViewClass)\n+```\n+\n+{% endmarkdown %}\n+{% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/docs/docs/base.html b/src/snek/docs/docs/base.html\nnew file mode 100644\nindex 0000000..7c53bec\n--- /dev/null\n+++ b/src/snek/docs/docs/base.html\n@@ -0,0 +1,116 @@\n+\n+\n+\n+ \n+ \n+\n+\n+\n+
\n+
\n+ Snek\n+ Docs\n+
\n+
\n+ {% block main %}\n+ {% endblock %}\n+
\n+
\n+
\n+ {% markdown %}\n+ {% endmarkdown %}\n+
\n+\n+\n+\n\\ No newline at end of file\ndiff --git a/src/snek/docs/docs/form_api_javascript.html b/src/snek/docs/docs/form_api_javascript.html\nnew file mode 100644\nindex 0000000..c83ee7f\n--- /dev/null\n+++ b/src/snek/docs/docs/form_api_javascript.html\n@@ -0,0 +1,17 @@\n+{% extends \"docs/base.html\" %}\n+\n+{% block main %}\n+{% markdown %}\n+\n+ - generic-form.js \n+\n+It's just a HTML component that can be declared using an one liner. Buttons and title are specified server side.\n+```html \n+\n+```\n+\n+{% endmarkdown %}\n+{% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/docs/docs/form_api_python.html b/src/snek/docs/docs/form_api_python.html\nnew file mode 100644\nindex 0000000..d7e67bc\n--- /dev/null\n+++ b/src/snek/docs/docs/form_api_python.html\n@@ -0,0 +1,92 @@\n+{% extends \"docs/base.html\" %}\n+\n+{% block main %}\n+{% markdown %}\n+\n+ - `snek.system.form.Form`\n+ - `snek.system.form.HTMLElement`\n+ - `snek.system.form.FormInputElement`\n+ - `snek.system.form.FormButtonElement`\n+\n+Here is an example with custom validation. \n+This example contains a field that checks if user already exists. \n+If invalid, it adds an error message which automatically invalidates the field. \n+Handling of the error messages will automatically done client side.\n+\n+Forms are usaly located in `snek/form/[form name].py`.\n+\n+```python\n+from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement\n+\n+class UsernameField(FormInputElement):\n+\n+ @property\n+ async def errors(self):\n+ result = await super().errors\n+ if self.value and await self.app.services.user.count(username=self.value):\n+ result.append(\"Username is not available.\")\n+ return result\n+\n+class RegisterForm(Form):\n+\n+ title = HTMLElement(tag=\"h1\", text=\"Register\")\n+\n+ username = UsernameField(\n+ name=\"username\", \n+ required=True,\n+ min_length=2,\n+ max_length=20,\n+ regex=r\"^[a-zA-Z0-9_]+$\",\n+ place_holder=\"Username\",\n+ type=\"text\"\n+ )\n+ email = FormInputElement(\n+ name=\"email\",\n+ required=False,\n+ regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n+ place_holder=\"Email address\",\n+ type=\"email\"\n+ )\n+ password = FormInputElement(\n+ name=\"password\",\n+ required=True,\n+ regex=r\"^[a-zA-Z0-9_.+-]{6,}\",\n+ type=\"password\",\n+ place_holder=\"Password\"\n+ )\n+ action = FormButtonElement(\n+ name=\"action\",\n+ value=\"submit\",\n+ text=\"Register\",\n+ type=\"button\"\n+ )\n+```\n+\n+\n+```python \n+data = dict(\n+ username=dict(value=\"retoor\"),\n+ password=dict(value=\"retoorded\")\n+)\n+form.set_user_data(data)\n+\n+is_valid = await form.is_valid\n+\n+key_value_values = await form.record \n+```\n+\n+{% endmarkdown %}\n+{% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/docs/docs/index.html b/src/snek/docs/docs/index.html\nnew file mode 100644\nindex 0000000..8ced8ab\n--- /dev/null\n+++ b/src/snek/docs/docs/index.html\n@@ -0,0 +1,37 @@\n+{% extends \"docs/base.html\" %}\n+\n+{% block main %}\n+{% markdown %}\n+\n+\n+Snek is a high customizable chat application. \n+It is made because Rocket Chat didn't fit my needs anymore. It became bloathed and very heavy commercialized. You would get upsell messages on your locally hosted instance!\n+\n+This documentation is under construction. Only the form API and the small introduction is a bit documented.\n+\n+[Small introduction / cheatsheet](/docs/docs/api.html)\n+\n+With the view classes of Snek you can render HTML and Markdown \n+\n+Snek's database model is based on Python dataset library. \n+Snek uses a model/mapper architecture build on top of that library.\n+\n+Snek does have his own components for creating and rendering forms. \n+All forms are made server side and client side is generated client side using a HTML component.\n+It's client side only one line to include a form that can validate and submit.\n+Validation is server side using REST. Page won't refresh.\n+[API Python](/docs/docs/form_api_python.html) \n+[API Javascript](/docs/docs/form_api_javascript.html)\n+\n+\n+{% endmarkdown %}\n+{% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Improve document handling and error responses", "commit": "dae877113c76b6f0eded7b2d63ef921123a2b559", "diff": "commit dae877113c76b6f0eded7b2d63ef921123a2b559\nAuthor: retoor \nDate: Fri Jan 24 23:42:24 2025 +0100\n\n Format.\n\ndiff --git a/src/snek/docs/app.py b/src/snek/docs/app.py\nindex 5e00d98..50a4245 100644\n--- a/src/snek/docs/app.py\n+++ b/src/snek/docs/app.py\n@@ -1,33 +1,43 @@\n-from app.app import Application as BaseApplication\n-import pathlib \n+import pathlib\n+\n from aiohttp import web\n+from app.app import Application as BaseApplication\n+\n from snek.system.markdown import MarkdownExtension\n \n-from snek.system.markdown import render_markdown \n \n class Application(BaseApplication):\n \n- def __init__(self, path=None, *args,**kwargs):\n+ def __init__(self, path=None, *args, **kwargs):\n self.path = pathlib.Path(path)\n template_path = self.path\n \n- super().__init__(template_path=template_path ,*args, **kwargs)\n- self.jinja2_env.add_extension(MarkdownExtension) \n- \n- self.router.add_get(\"/{tail:.*}\",self.handle_document)\n+ super().__init__(template_path=template_path, *args, **kwargs)\n+ self.jinja2_env.add_extension(MarkdownExtension)\n+\n+ self.router.add_get(\"/{tail:.*}\", self.handle_document)\n \n async def handle_document(self, request):\n- relative_path = request.match_info['tail'].strip(\"/\")\n- if relative_path == '':\n- relative_path = 'index.html'\n+ relative_path = request.match_info[\"tail\"].strip(\"/\")\n+ if relative_path == \"\":\n+ relative_path = \"index.html\"\n document_path = self.path.joinpath(relative_path)\n if not document_path.exists():\n- return web.Response(status=404,body=b'Resource is not found on this server.',content_type=\"text/plain\")\n+ return web.Response(\n+ status=404,\n+ body=b\"Resource is not found on this server.\",\n+ content_type=\"text/plain\",\n+ )\n if document_path.is_dir():\n document_path = document_path.joinpath(\"index.html\")\n if not document_path.exists():\n- return web.Response(status=404,body=b'Resource is not found on this server.',content_type=\"text/plain\")\n- \n- response = await self.render_template(str(document_path.relative_to(self.path)),request)\n- return response\n+ return web.Response(\n+ status=404,\n+ body=b\"Resource is not found on this server.\",\n+ content_type=\"text/plain\",\n+ )\n \n+ response = await self.render_template(\n+ str(document_path.relative_to(self.path)), request\n+ )\n+ return response"} +{"repo": ".", "date": "2025-01-25", "line": "feat: Added session support and login functionality", "commit": "5c69e14d7cfae8da4efab776165cc8e466edcc41", "diff": "commit 5c69e14d7cfae8da4efab776165cc8e466edcc41\nAuthor: retoor \nDate: Sat Jan 25 03:46:33 2025 +0100\n\n Added session support.\n\ndiff --git a/cache/crc321300331366.cache b/cache/crc321300331366.cache\ndeleted file mode 100644\nindex e69de29..0000000\ndiff --git a/cache/crc322507170282.cache b/cache/crc322507170282.cache\ndeleted file mode 100644\nindex e69de29..0000000\ndiff --git a/pyproject.toml b/pyproject.toml\nindex cc36846..71e90a7 100644\n--- a/pyproject.toml\n+++ b/pyproject.toml\n@@ -21,7 +21,8 @@ dependencies = [\n \"gunicorn\",\n \"imgkit\",\n \"wkhtmltopdf\",\n- \"jinja-markdown2\",\n- \"mistune\"\n+ \"mistune\",\n+ \"aiohttp-session\",\n+ \"cryptography\"\n ]\n \ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex ab19f42..0abf24b 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -17,8 +17,23 @@ from snek.view.login import LoginView\n from snek.view.login_form import LoginFormView\n from snek.view.register import RegisterView\n from snek.view.register_form import RegisterFormView\n+from snek.view.status import StatusView\n from snek.view.web import WebView\n \n+from aiohttp import web\n+from aiohttp_session import setup as session_setup, get_session as session_get, session_middleware\n+from aiohttp_session.cookie_storage import EncryptedCookieStorage\n+import base64\n+\n+SESSION_KEY = b'c79a0c5fda4b424189c427d28c9f7c34'\n+\n+@web.middleware\n+async def session_middleware(request, handler):\n+ setattr(request,\"session\", await session_get(request))\n+ response = await handler(request)\n+ return response\n+\n \n class Application(BaseApplication):\n \n@@ -31,10 +46,13 @@ class Application(BaseApplication):\n super().__init__(\n middlewares=middlewares, template_path=self.template_path, *args, **kwargs\n )\n+ session_setup(self,EncryptedCookieStorage(SESSION_KEY))\n+ self._middlewares.append(session_middleware)\n self.jinja2_env.add_extension(MarkdownExtension)\n self.setup_router()\n self.setup_services()\n \n+ \n def setup_services(self):\n self.services = SimpleNamespace(**get_services(app=self))\n self.mappers = SimpleNamespace(**get_mappers(app=self))\n@@ -51,7 +69,7 @@ class Application(BaseApplication):\n self.router.add_view(\"/about.md\", AboutMDView)\n self.router.add_view(\"/docs.html\", DocsHTMLView)\n self.router.add_view(\"/docs.md\", DocsMDView)\n-\n+ self.router.add_view(\"/status.json\",StatusView)\n self.router.add_view(\"/web.html\", WebView)\n self.router.add_view(\"/login.html\", LoginView)\n self.router.add_view(\"/login.json\", LoginFormView)\ndiff --git a/src/snek/docs/__pycache__/app.cpython-312.pyc b/src/snek/docs/__pycache__/app.cpython-312.pyc\nindex 087bb64..6f355a6 100644\nBinary files a/src/snek/docs/__pycache__/app.cpython-312.pyc and b/src/snek/docs/__pycache__/app.cpython-312.pyc differ\ndiff --git a/src/snek/static/app.js b/src/snek/static/app.js\nindex f06ee24..d8d3a8f 100644\n--- a/src/snek/static/app.js\n+++ b/src/snek/static/app.js\n@@ -67,7 +67,7 @@ class Room {\n class InlineAppElement extends HTMLElement {\n \n constructor(){\n- this.\n }\n \n }\n@@ -77,6 +77,45 @@ class Page {\n \n }\n \n+class RESTClient {\n+ debug = true \n+ \n+ async get(url, params){\n+ params = params ? params : {} \n+ const encodedParams = new URLSearchParams(params);\n+ if(encodedParams)\n+ url += '?' + encodedParams\n+ const response = await fetch(url,{\n+ method: 'GET',\n+ headers: {\n+ 'Content-Type': 'application/json'\n+ }\n+ });\n+ const result = await response.json()\n+ if(this.debug){\n+ console.debug({url:url,params:params,result:result})\n+ }\n+ return result \n+ }\n+ async post(url, data) {\n+ const response = await fetch(url,{\n+ method: 'POST',\n+ headers: {\n+ 'Content-Type': 'application/json'\n+ },\n+ body: JSON.stringify(data)\n+ });\n+\n+ const result = await response.json()\n+ if(this.debug){\n+ console.debug({url:url,params:params,result:result})\n+ }\n+ return result\n+ }\n+}\n+\n+const rest = new RESTClient()\n+\n class App {\n rooms = []\n constructor() {\n@@ -84,7 +123,9 @@ class App {\n \n \n }\n+ async post(url, data){\n \n+ }\n \n \n }\n\\ No newline at end of file\ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nindex a47c6db..e775bf4 100644\n--- a/src/snek/static/generic-form.js\n+++ b/src/snek/static/generic-form.js\n@@ -282,8 +282,10 @@ class GenericForm extends HTMLElement {\n {\n const isValid = await me.validate()\n if(isValid){\n- const isProcessed = await me.submit()\n- console.info({processed:isProcessed})\n+ const saveResult = await me.submit()\n+ if(saveResult.redirect_url){\n+ window.location.pathname = saveResult.redirect_url\n+ }\n }\n }\n }\n@@ -315,7 +317,6 @@ class GenericForm extends HTMLElement {\n if(!field.is_valid){\n me.fields[field.name].setInvalid()\n me.fields[field.name].setErrors(field.errors)\n- console.info(field.name,\"is invalid\")\n }else{\n me.fields[field.name].setValid()\n }\n@@ -323,10 +324,8 @@ class GenericForm extends HTMLElement {\n me.fields[field.name].updateAttributes()\n })\n Object.values(form.fields).forEach(field=>{\n- console.info(field.errors)\n me.fields[field.name].setErrors(field.errors)\n })\n- console.info({XX:form})\n return form['is_valid']\n }\n async submit(){\ndiff --git a/src/snek/system/model.py b/src/snek/system/model.py\nindex b41f4ba..98729b2 100644\n--- a/src/snek/system/model.py\n+++ b/src/snek/system/model.py\n@@ -212,6 +212,14 @@ class DeletedField(ModelField):\n \n class UUIDField(ModelField):\n \n+ @property\n+ def value(self):\n+ return str(self._value)\n+ \n+ @value.setter\n+ def value(self,val):\n+ self._value = str(val)\n+\n @property\n def initial_value(self):\n return str(uuid.uuid4())\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nindex 1cf5329..8eebcb0 100644\n--- a/src/snek/system/view.py\n+++ b/src/snek/system/view.py\n@@ -5,6 +5,13 @@ from snek.system.markdown import render_markdown\n \n class BaseView(web.View):\n \n+ login_required = False\n+\n+ async def _iter(self):\n+ if self.login_required and not self.session.get(\"logged_in\"):\n+ return web.HTTPFound(\"/\")\n+ return await super()._iter()\n+\n @property\n def app(self):\n return self.request.app\n@@ -16,6 +23,10 @@ class BaseView(web.View):\n async def json_response(self, data):\n return web.json_response(data)\n \n+ @property \n+ def session(self):\n+ return self.request.session\n+\n async def render_template(self, template_name, context=None):\n if template_name.endswith(\".md\"):\n response = await self.request.app.render_template(\n@@ -46,7 +57,8 @@ class BaseFormView(BaseView):\n pass\n if post.get(\"action\") == \"submit\" and result[\"is_valid\"]:\n- await self.submit(form)\n+ result = await self.submit(form)\n+ return await self.json_response(result)\n return await self.json_response(result)\n \n async def submit(self, model=None):\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex e00c886..cb8fc5d 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -4,6 +4,7 @@\n \n \n {% block title %}{% endblock %}\n+ \n \n \n \ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex 0403e1b..5a636f0 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -3,12 +3,13 @@\n \n \n \n- Dark Themed Chat Application\n+ Snek\n+ \n \n \n \n
\n-
Molodetz Chat
\n+
Snek
\n
\n
\n {% block sidebar %}\n- \n+ {% include \"sidebar_channels.html\" %}\n {% endblock %}\n {% block main %}\n \ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex fb935c8..bdcddae 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -132,6 +132,7 @@\n app.addEventListener(\"channel-message\", (data) => {\n if (data.channel_uid !== channelUid) {\n if(!isMentionForSomeoneElse(data.message)){\n+ channelSidebar.notify(data);\n app.playSound(\"messageOtherChannel\");\n }\n \ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex dda589f..5a3b264 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -52,7 +52,7 @@ class WebView(BaseView):\n other_user = await self.app.services.channel_member.get_other_dm_user(subscribed_channel[\"channel_uid\"], self.session.get(\"uid\"))\n if other_user:\n item[\"name\"] = other_user[\"nick\"]\n- item[\"uid\"] = other_user[\"uid\"]\n+ item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n else:\n item[\"name\"] = subscribed_channel[\"label\"]\n item[\"uid\"] = subscribed_channel[\"channel_uid\"]"} +{"repo": ".", "date": "2025-02-22", "line": "feat: Added channel sidebar with message counts and highlighting", "commit": "076fbb30fb51ecfb5b15d394c760f55dac26e1c1", "diff": "commit 076fbb30fb51ecfb5b15d394c760f55dac26e1c1\nAuthor: retoor \nDate: Sat Feb 22 01:21:57 2025 +0100\n\n Added sidebar stuff.\n\ndiff --git a/src/snek/templates/sidebar_channels.html b/src/snek/templates/sidebar_channels.html\nnew file mode 100644\nindex 0000000..ed16ffd\n--- /dev/null\n+++ b/src/snek/templates/sidebar_channels.html\n@@ -0,0 +1,64 @@\n+\n+\n+ \n\\ No newline at end of file"} +{"repo": ".", "date": "2025-02-26", "line": "feat: Added avatar generation and display\n\nThis commit introduces avatar generation using the `multiavatar` library.\n\nChanges:\n\n- Added `multiavatar` as a dependency in `pyproject.toml`.\n- Created a new `AvatarView` to serve avatar images.\n- Updated `message.html` to display avatar images instead of initials.\n- Added a new route `/avatar/{uid}.svg` in `src/snek/app.py`.\n- Added caching headers to avatar and drive item responses.\n", "commit": "5af4e5754b6902ae13c798d9793281d62b684590", "diff": "commit 5af4e5754b6902ae13c798d9793281d62b684590\nAuthor: retoor \nDate: Wed Feb 26 00:22:36 2025 +0100\n\n Update avatar.\n\ndiff --git a/Makefile b/Makefile\nindex f153fc1..e3febf7 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -5,13 +5,15 @@ GUNICORN=./.venv/bin/gunicorn\n GUNICORN_WORKERS = 1\n PORT = 8081\n \n+python:\n+\t$(PYTHON)\n \n+run:\n+\t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\n+\t\n install:\n \tpython3 -m venv .venv \n \t$(PIP) install -e .\n \n \n \n-run:\n-\t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\n-\t\ndiff --git a/pyproject.toml b/pyproject.toml\nindex 2d5b6d9..e1075c9 100644\n--- a/pyproject.toml\n+++ b/pyproject.toml\n@@ -28,6 +28,7 @@ dependencies = [\n \"asyncssh\",\n \"emoji\",\n \"aiofiles\",\n- \"PyJWT\"\n+ \"PyJWT\",\n+ \"multiavatar\"\n ]\n \ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 3a61c2b..99f23e7 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -33,8 +33,10 @@ from snek.view.status import StatusView\n from snek.view.web import WebView\n from snek.view.upload import UploadView\n from snek.view.search_user import SearchUserView \n+from snek.view.avatar import AvatarView\n from snek.system.profiler import profiler_handler\n \n+\n SESSION_KEY = b\"c79a0c5fda4b424189c427d28c9f7c34\"\n \n \n@@ -101,6 +103,7 @@ class Application(BaseApplication):\n self.router.add_view(\"/drive.bin/{uid}.{ext}\", UploadView)\n self.router.add_view(\"/search-user.html\", SearchUserView)\n self.router.add_view(\"/search-user.json\", SearchUserView)\n+ self.router.add_view(\"/avatar/{uid}.svg\", AvatarView)\n self.router.add_get(\"/http-get\", self.handle_http_get)\n self.router.add_get(\"/http-photo\", self.handle_http_photo)\n self.router.add_get(\"/rpc.ws\", RPCView)\ndiff --git a/src/snek/templates/message.html b/src/snek/templates/message.html\nindex 2773f0a..e8afc31 100644\n--- a/src/snek/templates/message.html\n+++ b/src/snek/templates/message.html\n@@ -1 +1 @@\n-
{{user_nick[0]}}
{{user_nick}}
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}
\n+
{{user_nick}}
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}
\ndiff --git a/src/snek/view/avatar.py b/src/snek/view/avatar.py\nnew file mode 100644\nindex 0000000..065ff6a\n--- /dev/null\n+++ b/src/snek/view/avatar.py\n@@ -0,0 +1,39 @@\n+\n+\n+\n+from multiavatar import multiavatar\n+\n+from aiohttp import web\n+from snek.system.view import BaseView\n+\n+class AvatarView(BaseView):\n+ login_required = True\n+\n+ async def get(self):\n+ uid = self.request.match_info.get(\"uid\")\n+ avatar = multiavatar.multiavatar(uid,None, None)\n+ response = web.Response(text=avatar, content_type='image/svg+xml')\n+ response.headers['Cache-Control'] = f'public, max-age={1337*42}'\n+ return response\ndiff --git a/src/snek/view/upload.py b/src/snek/view/upload.py\nindex 05ffcaa..a80ec72 100644\n--- a/src/snek/view/upload.py\n+++ b/src/snek/view/upload.py\n@@ -23,7 +23,9 @@ class UploadView(BaseView):\n async def get(self):\n uid = self.request.match_info.get(\"uid\")\n drive_item = await self.services.drive_item.get(uid)\n- return web.FileResponse(drive_item[\"path\"])\n+ response = web.FileResponse(drive_item[\"path\"])\n+ response.headers['Cache-Control'] = f'public, max-age={1337*420}'\n+ return response\n \n async def post(self):\n reader = await self.request.multipart()"} +{"repo": ".", "date": "2025-02-26", "line": "feat: Generate unique avatar UIDs when requested", "commit": "e280e8776457605bbb5548fe9de5328b7b04bb8a", "diff": "commit e280e8776457605bbb5548fe9de5328b7b04bb8a\nAuthor: retoor \nDate: Wed Feb 26 00:40:18 2025 +0100\n\n Update avatar.\n\ndiff --git a/src/snek/view/avatar.py b/src/snek/view/avatar.py\nindex 065ff6a..8281df1 100644\n--- a/src/snek/view/avatar.py\n+++ b/src/snek/view/avatar.py\n@@ -24,7 +24,7 @@\n from multiavatar import multiavatar\n-\n+import uuid\n from aiohttp import web\n from snek.system.view import BaseView\n \n@@ -33,6 +33,8 @@ class AvatarView(BaseView):\n \n async def get(self):\n uid = self.request.match_info.get(\"uid\")\n+ if uid == \"unique\":\n+ uid = str(uuid.uuid4())\n avatar = multiavatar.multiavatar(uid,None, None)\n response = web.Response(text=avatar, content_type='image/svg+xml')\n response.headers['Cache-Control'] = f'public, max-age={1337*42}'"} +{"repo": ".", "date": "2025-02-26", "line": "feat: Generate unique avatars", "commit": "162cfe394558a085894a11cea57b193d6108b90e", "diff": "commit 162cfe394558a085894a11cea57b193d6108b90e\nAuthor: retoor \nDate: Wed Feb 26 00:46:48 2025 +0100\n\n Update avatar.\n\ndiff --git a/src/snek/view/avatar.py b/src/snek/view/avatar.py\nindex 8281df1..54dbec0 100644\n--- a/src/snek/view/avatar.py\n+++ b/src/snek/view/avatar.py\n@@ -35,7 +35,7 @@ class AvatarView(BaseView):\n uid = self.request.match_info.get(\"uid\")\n if uid == \"unique\":\n uid = str(uuid.uuid4())\n- avatar = multiavatar.multiavatar(uid,None, None)\n+ avatar = multiavatar.multiavatar(uid, True, None)\n response = web.Response(text=avatar, content_type='image/svg+xml')\n response.headers['Cache-Control'] = f'public, max-age={1337*42}'\n return response"} +{"repo": ".", "date": "2025-02-26", "line": "feat: Updated welcome message and registration buttons", "commit": "da1be6301c79cf76383e6568f91ee23bdf5119f6", "diff": "commit da1be6301c79cf76383e6568f91ee23bdf5119f6\nAuthor: retoor \nDate: Wed Feb 26 06:21:48 2025 +0100\n\n Text\n\ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nindex c6d57df..1894bc4 100644\n--- a/src/snek/templates/index.html\n+++ b/src/snek/templates/index.html\n@@ -11,12 +11,11 @@\n \n
\n

Snek

\n+

Rocket Chat got bloated, too commercialized,\n+ So Snek came through, lean and optimized.

\n \n- Or\n+ OR\n \n- Design choices\n- App preview\n- API docs\n
\n \n "} +{"repo": ".", "date": "2025-02-28", "line": "fix: Disable login requirement for avatar view", "commit": "66b85d146abac25df83edc1975db209b9d43fae7", "diff": "commit 66b85d146abac25df83edc1975db209b9d43fae7\nAuthor: retoor \nDate: Fri Feb 28 15:42:52 2025 +0100\n\n Non required avatar stuff.\n\ndiff --git a/src/snek/view/avatar.py b/src/snek/view/avatar.py\nindex 54dbec0..cbd973c 100644\n--- a/src/snek/view/avatar.py\n+++ b/src/snek/view/avatar.py\n@@ -29,7 +29,7 @@ from aiohttp import web\n from snek.system.view import BaseView\n \n class AvatarView(BaseView):\n- login_required = True\n+ login_required = False\n \n async def get(self):\n uid = self.request.match_info.get(\"uid\")"} +{"repo": ".", "date": "2025-03-02", "line": "feat: Add last_message_on to ChannelModel and update on message send", "commit": "4620ebb800b5dd848ec28713f1afa20416698922", "diff": "commit 4620ebb800b5dd848ec28713f1afa20416698922\nAuthor: retoor \nDate: Sun Mar 2 14:49:38 2025 +0100\n\n Channel check.\n\ndiff --git a/src/snek/model/channel.py b/src/snek/model/channel.py\nindex d664087..8a40ced 100644\n--- a/src/snek/model/channel.py\n+++ b/src/snek/model/channel.py\n@@ -9,3 +9,4 @@ class ChannelModel(BaseModel):\n is_private = ModelField(name=\"is_private\", required=True, kind=bool, value=False)\n is_listed = ModelField(name=\"is_listed\", required=True, kind=bool, value=True)\n index = ModelField(name=\"index\", required=True, kind=int, value=1000)\n+ last_message_on = ModelField(name=\"last_message_on\", required=False, kind=str)\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex 02a4bd5..5eacbeb 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -1,12 +1,16 @@\n \n \n \n+from snek.system.model import now\n from snek.system.service import BaseService\n \n \n class ChatService(BaseService):\n \n async def send(self,user_uid, channel_uid, message):\n+ channel = await self.services.channel.get(uid=channel_uid)\n+ if not channel:\n+ raise Exception(\"Channel not found.\")\n channel_message = await self.services.channel_message.create(\n channel_uid, \n user_uid, \n@@ -28,4 +32,6 @@ class ChatService(BaseService):\n uid=channel_message['uid'],\n user_nick=user['nick']\n ))\n+ channel['last_message_on'] = now()\n+ await self.services.channel.save(channel)\n return sent_to_count\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-03-02", "line": "feat: Add index creation with error handling", "commit": "e469e27abfedc0b08b483e0715b4dc9b16240c5e", "diff": "commit e469e27abfedc0b08b483e0715b4dc9b16240c5e\nAuthor: retoor \nDate: Sun Mar 2 14:59:25 2025 +0100\n\n Optinal indexes.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 99f23e7..6d9346d 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -67,13 +67,17 @@ class Application(BaseApplication):\n self.setup_router()\n self.db.query(\"PRAGMA journal_mode=WAL\")\n self.db.query(\"PRAGMA syncnorm=off\")\n- if not self.db[\"user\"].has_index(\"username\"):\n- self.db[\"user\"].create_index(\"username\", unique=True)\n- if not self.db[\"channel_member\"].has_index([\"channel_uid\",\"user_uid\"]):\n- self.db[\"channel_member\"].create_index([\"channel_uid\",\"user_uid\"])\n- if not self.db[\"channel_message\"].has_index([\"channel_uid\",\"user_uid\"]):\n- self.db[\"channel_message\"].create_index([\"channel_uid\",\"user_uid\"])\n- \n+\n+ try:\n+ if not self.db[\"user\"].has_index(\"username\"):\n+ self.db[\"user\"].create_index(\"username\", unique=True)\n+ if not self.db[\"channel_member\"].has_index([\"channel_uid\",\"user_uid\"]):\n+ self.db[\"channel_member\"].create_index([\"channel_uid\",\"user_uid\"])\n+ if not self.db[\"channel_message\"].has_index([\"channel_uid\",\"user_uid\"]):\n+ self.db[\"channel_message\"].create_index([\"channel_uid\",\"user_uid\"])\n+ except:\n+ pass \n+ \n self.cache = Cache(self)\n self.services = get_services(app=self)\n self.mappers = get_mappers(app=self)"} +{"repo": ".", "date": "2025-03-03", "line": "fix: Corrected link and upload display formatting", "commit": "45e3239cc06cdab0a8e5c1c1ef56593f65e750ea", "diff": "commit 45e3239cc06cdab0a8e5c1c1ef56593f65e750ea\nAuthor: retoor \nDate: Mon Mar 3 00:35:08 2025 +0100\n\n Upload changes.\n\ndiff --git a/src/snek/system/template.py b/src/snek/system/template.py\nindex 22d007d..0e71b80 100644\n--- a/src/snek/system/template.py\n+++ b/src/snek/system/template.py\n@@ -73,7 +73,7 @@ def set_link_target_blank(text):\n element.attrs['target'] = '_blank'\n element.attrs['rel'] = 'noopener noreferrer'\n element.attrs['referrerpolicy'] = 'no-referrer'\n- element.attrs['href'] = element.attrs['href'].strip(\".\")\n+ element.attrs['href'] = element.attrs['href'].strip(\".\").strip(\",\")\n \n return str(soup)\n \ndiff --git a/src/snek/view/upload.py b/src/snek/view/upload.py\nindex a80ec72..f9bad33 100644\n--- a/src/snek/view/upload.py\n+++ b/src/snek/view/upload.py\n@@ -77,7 +77,7 @@ class UploadView(BaseView):\n await self.services.drive_item.save(drive_item)\n response = \"Uploaded [\" + filename + \"](/drive.bin/\" + drive_item[\"uid\"] + \")\"\n- response = \"[url](/drive.bin/\" + drive_item[\"uid\"] + extension + \")\"\n+ response = \"[\" + filename + \"](/drive.bin/\" + drive_item[\"uid\"] + extension + \")\"\n \n await self.services.chat.send(\n self.request.session.get(\"uid\"), channel_uid, response"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Added push notifications to service worker", "commit": "c3c94461c295ef4c6219051369472f983267437c", "diff": "commit c3c94461c295ef4c6219051369472f983267437c\nAuthor: retoor \nDate: Wed Mar 5 17:19:24 2025 +0100\n\n Update service worker.\n\ndiff --git a/src/snek/static/service-worker.js b/src/snek/static/service-worker.js\nindex dfdc493..5881455 100644\n--- a/src/snek/static/service-worker.js\n+++ b/src/snek/static/service-worker.js\n@@ -1,3 +1,37 @@\n+async function requestNotificationPermission() {\n+ const permission = await Notification.requestPermission();\n+ return permission === 'granted';\n+}\n+\n+async function subscribeUser() {\n+ const registration = await navigator.serviceWorker.register('/service-worker.js');\n+ \n+ const subscription = await registration.pushManager.subscribe({\n+ userVisibleOnly: true,\n+ applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)\n+ });\n+\n+ await fetch('/subscribe', {\n+ method: 'POST',\n+ body: JSON.stringify(subscription),\n+ headers: {\n+ 'Content-Type': 'application/json'\n+ }\n+ });\n+}\n+\n+self.addEventListener('push', event => {\n+ const data = event.data.json();\n+ self.registration.showNotification(data.title, {\n+ body: data.message,\n+ icon: data.icon\n+ });\n+});\n+\n self.addEventListener(\"install\", (event) => {\n console.log(\"Service worker installed\");\n });\n@@ -27,4 +61,4 @@ self.addEventListener(\"notificationclick\", (event) => {\n event.notification.close();\n event.waitUntil(clients.openWindow(\n-});"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Implement notification read functionality and unread stats", "commit": "578c182f2707a5f5b7c93f421e2035f7271aa60c", "diff": "commit 578c182f2707a5f5b7c93f421e2035f7271aa60c\nAuthor: retoor \nDate: Wed Mar 5 17:51:25 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 3815c60..27c65f4 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -1,9 +1,17 @@\n from snek.system.service import BaseService\n-\n+from snek.system.model import now\n \n class NotificationService(BaseService):\n mapper_name = \"notification\"\n \n+ async def mark_as_read(self, user_uid, channel_message_uid):\n+ model = await self.get(user_uid, object_uid=channel_message_uid)\n+ model['read_at'] = now()\n+ await self.save(model)\n+\n+ async def get_unread_stats(self,user_uid):\n+ 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))\n+\n async def create(self, object_uid, object_type, user_uid, message):\n model = await self.new()\n model[\"object_uid\"] = object_uid\ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex d98e53b..c197f24 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -39,6 +39,11 @@ class RPCView(BaseView):\n def is_logged_in(self):\n return self.view.session.get(\"logged_in\", False)\n \n+ async def mark_as_read(self, message_uid):\n+ self._require_login()\n+ await self.services.notification.mark_as_read(self.user_uid, message_uid)\n+ return True\n+\n async def login(self, username, password):\n success = await self.services.user.validate_login(username, password)\n if not success:\ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex 5a3b264..8fd5ddc 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -46,6 +46,9 @@ class WebView(BaseView):\n messages = [await self.app.services.channel_message.to_extended_dict(message) for message in await self.app.services.channel_message.offset(\n channel[\"uid\"]\n )]\n+ for message in messages:\n+ await self.app.services.notification.mark_as_read(self.session.get(\"uid\"),message[\"uid\"])\n+\n channels = []\n async for subscribed_channel in self.app.services.channel_member.find(user_uid=self.session.get(\"uid\"), deleted_at=None, is_banned=False):\n item = {}"} +{"repo": ".", "date": "2025-03-05", "line": "fix: Handle missing notification model in mark_as_read", "commit": "580ec5ab0d57a542ab38b25a6c508804d5bcfa21", "diff": "commit 580ec5ab0d57a542ab38b25a6c508804d5bcfa21\nAuthor: retoor \nDate: Wed Mar 5 17:52:18 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 27c65f4..6db762c 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -6,9 +6,12 @@ class NotificationService(BaseService):\n \n async def mark_as_read(self, user_uid, channel_message_uid):\n model = await self.get(user_uid, object_uid=channel_message_uid)\n+ if not model:\n+ return False \n model['read_at'] = now()\n await self.save(model)\n-\n+ return True \n+ \n async def get_unread_stats(self,user_uid):\n 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))"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Add new_count to ChannelMember and increment on notification", "commit": "84d7b11f24b37cdc41ce9d5bb24be4080af14be9", "diff": "commit 84d7b11f24b37cdc41ce9d5bb24be4080af14be9\nAuthor: retoor \nDate: Wed Mar 5 17:59:59 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/model/channel_member.py b/src/snek/model/channel_member.py\nindex d199498..65ba3e4 100644\n--- a/src/snek/model/channel_member.py\n+++ b/src/snek/model/channel_member.py\n@@ -13,3 +13,4 @@ class ChannelMemberModel(BaseModel):\n )\n is_muted = ModelField(name=\"is_muted\", required=True, kind=bool, value=False)\n is_banned = ModelField(name=\"is_banned\", required=True, kind=bool, value=False)\n+ new_count = ModelField(name=\"new_count\", required=False, kind=int, value=0)\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 6db762c..ad0acbe 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -36,6 +36,8 @@ class NotificationService(BaseService):\n is_muted=False,\n deleted_at=None,\n ):\n+ channel_member['new_count'] += 1\n+ await self.services.channel_member.save(channel_member)\n model = await self.new()\n model[\"object_uid\"] = channel_message_uid\n model[\"object_type\"] = \"channel_message\""} +{"repo": ".", "date": "2025-03-05", "line": "feat: Display new notification count in RPC view", "commit": "d7851e645785fd707a7c7fdc5b6fff036e0c80f7", "diff": "commit d7851e645785fd707a7c7fdc5b6fff036e0c80f7\nAuthor: retoor \nDate: Wed Mar 5 18:02:02 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex c197f24..55accc7 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -94,6 +94,7 @@ class RPCView(BaseView):\n \"name\": subscription[\"label\"],\n \"uid\": subscription[\"channel_uid\"],\n \"tag\": channel[\"tag\"],\n+ \"new_count\": subscription[\"new_count\"],\n \"is_moderator\": subscription[\"is_moderator\"],\n \"is_read_only\": subscription[\"is_read_only\"]\n })"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Increment new_count for channel messages and members", "commit": "e1324e99bf06018a804a1a3dcc83f96cde04b1af", "diff": "commit e1324e99bf06018a804a1a3dcc83f96cde04b1af\nAuthor: retoor \nDate: Wed Mar 5 18:05:34 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex 5eacbeb..54e60dc 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -17,7 +17,10 @@ class ChatService(BaseService):\n message\n )\n channel_message_uid = channel_message[\"uid\"]\n- \n+ if not channel_message['new_count']:\n+ channel_message['new_count'] = 0\n+ channel_message['new_count'] += 1\n+ await self.services.channel_message.save(channel_message)\n user = await self.services.user.get(uid=user_uid)\n await self.services.notification.create_channel_message(channel_message_uid)\n sent_to_count = await self.services.socket.broadcast(channel_uid, dict(\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex ad0acbe..6db762c 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -36,8 +36,6 @@ class NotificationService(BaseService):\n is_muted=False,\n deleted_at=None,\n ):\n- channel_member['new_count'] += 1\n- await self.services.channel_member.save(channel_member)\n model = await self.new()\n model[\"object_uid\"] = channel_message_uid\n model[\"object_type\"] = \"channel_message\""} +{"repo": ".", "date": "2025-03-05", "line": "feat: Increment new_count in chat and notification services", "commit": "afbf53938bd59e1e03dfc011063b27134dd0c054", "diff": "commit afbf53938bd59e1e03dfc011063b27134dd0c054\nAuthor: retoor \nDate: Wed Mar 5 18:09:54 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex 54e60dc..17c677b 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -17,10 +17,8 @@ class ChatService(BaseService):\n message\n )\n channel_message_uid = channel_message[\"uid\"]\n- if not channel_message['new_count']:\n- channel_message['new_count'] = 0\n- channel_message['new_count'] += 1\n- await self.services.channel_message.save(channel_message)\n+ \n+ \n user = await self.services.user.get(uid=user_uid)\n await self.services.notification.create_channel_message(channel_message_uid)\n sent_to_count = await self.services.socket.broadcast(channel_uid, dict(\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 6db762c..a703f23 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -36,6 +36,11 @@ class NotificationService(BaseService):\n is_muted=False,\n deleted_at=None,\n ):\n+ if not channel_member['new_count']:\n+ channel_member['new_count'] = 0\n+ channel_member['new_count'] += 1\n+ await self.services.channel_member.save(channel_member)\n+ \n model = await self.new()\n model[\"object_uid\"] = channel_message_uid\n model[\"object_type\"] = \"channel_message\""} +{"repo": ".", "date": "2025-03-05", "line": "feat: Added new_count to notifications", "commit": "8d3d7327d777ae3150ecaa237e137f8221310dfa", "diff": "commit 8d3d7327d777ae3150ecaa237e137f8221310dfa\nAuthor: retoor \nDate: Wed Mar 5 18:17:43 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex a703f23..f5917e7 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -21,6 +21,7 @@ class NotificationService(BaseService):\n model[\"object_type\"] = object_type\n model[\"user_uid\"] = user_uid\n model[\"message\"] = message\n+ model['new_count'] = 1337\n if await self.save(model):\n return model\n raise Exception(f\"Failed to create notification: {model.errors}.\")\n@@ -40,7 +41,7 @@ class NotificationService(BaseService):\n channel_member['new_count'] = 0\n channel_member['new_count'] += 1\n await self.services.channel_member.save(channel_member)\n- \n+\n model = await self.new()\n model[\"object_uid\"] = channel_message_uid\n model[\"object_type\"] = \"channel_message\""} +{"repo": ".", "date": "2025-03-05", "line": "feat: Remove unused new_count field from notification model", "commit": "4df6055566d61c769ae1759d81900d138093136e", "diff": "commit 4df6055566d61c769ae1759d81900d138093136e\nAuthor: retoor \nDate: Wed Mar 5 18:18:06 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex f5917e7..399a0a1 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -21,7 +21,6 @@ class NotificationService(BaseService):\n model[\"object_type\"] = object_type\n model[\"user_uid\"] = user_uid\n model[\"message\"] = message\n- model['new_count'] = 1337\n if await self.save(model):\n return model\n raise Exception(f\"Failed to create notification: {model.errors}.\")"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Increment notification count and add debug print", "commit": "dd11c8da5acc5ed97a278a705e7282edc7d50bc5", "diff": "commit dd11c8da5acc5ed97a278a705e7282edc7d50bc5\nAuthor: retoor \nDate: Wed Mar 5 18:18:48 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 399a0a1..5a301a8 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -39,6 +39,7 @@ class NotificationService(BaseService):\n if not channel_member['new_count']:\n channel_member['new_count'] = 0\n channel_member['new_count'] += 1\n+ print(\"INSERTED!!\",flush=True)\n await self.services.channel_member.save(channel_member)\n \n model = await self.new()"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Log model records during upsert", "commit": "8f137cf8e6d67a8e73ee221d2f9192b7a3ab431d", "diff": "commit 8f137cf8e6d67a8e73ee221d2f9192b7a3ab431d\nAuthor: retoor \nDate: Wed Mar 5 18:24:32 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 5a301a8..399a0a1 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -39,7 +39,6 @@ class NotificationService(BaseService):\n if not channel_member['new_count']:\n channel_member['new_count'] = 0\n channel_member['new_count'] += 1\n- print(\"INSERTED!!\",flush=True)\n await self.services.channel_member.save(channel_member)\n \n model = await self.new()\ndiff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py\nindex d7e4163..e57b0fa 100644\n--- a/src/snek/system/mapper.py\n+++ b/src/snek/system/mapper.py\n@@ -49,6 +49,7 @@ class BaseMapper:\n if not model.record.get(\"uid\"):\n raise Exception(f\"Attempt to save without uid: {model.record}.\")\n model.updated_at.update()\n+ print(model.record,flush=True)\n return self.table.upsert(model.record, [\"uid\"])\n \n async def find(self, **kwargs) -> typing.AsyncGenerator:"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Allow channel members to be created", "commit": "30fe0bfae7f2bc9badcb16f734655e661fe976ad", "diff": "commit 30fe0bfae7f2bc9badcb16f734655e661fe976ad\nAuthor: retoor \nDate: Wed Mar 5 18:37:44 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/channel_member.py b/src/snek/service/channel_member.py\nindex f34f08b..191a063 100644\n--- a/src/snek/service/channel_member.py\n+++ b/src/snek/service/channel_member.py\n@@ -5,6 +5,7 @@ class ChannelMemberService(BaseService):\n \n mapper_name = \"channel_member\"\n \n+\n async def create(\n self,\n channel_uid,\ndiff --git a/src/snek/system/service.py b/src/snek/system/service.py\nindex d65f947..9feb759 100644\n--- a/src/snek/system/service.py\n+++ b/src/snek/system/service.py\n@@ -61,7 +61,7 @@ class BaseService:\n if not \"_limit\" in kwargs or int(kwargs.get(\"_limit\")) > 30:\n kwargs[\"_limit\"] = 60\n async for model in self.mapper.find(**kwargs):\n- yield model\n+ yield await self.get(uid=model[\"uid\"])\n \n async def delete(self, **kwargs):\n return await self.mapper.delete(**kwargs)"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Return model directly from query", "commit": "edb35b57070a5659e8491a967880d816f2d07697", "diff": "commit edb35b57070a5659e8491a967880d816f2d07697\nAuthor: retoor \nDate: Wed Mar 5 18:40:53 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/system/service.py b/src/snek/system/service.py\nindex 9feb759..d65f947 100644\n--- a/src/snek/system/service.py\n+++ b/src/snek/system/service.py\n@@ -61,7 +61,7 @@ class BaseService:\n if not \"_limit\" in kwargs or int(kwargs.get(\"_limit\")) > 30:\n kwargs[\"_limit\"] = 60\n async for model in self.mapper.find(**kwargs):\n- yield await self.get(uid=model[\"uid\"])\n+ yield model\n \n async def delete(self, **kwargs):\n return await self.mapper.delete(**kwargs)"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Display username when inserting notification", "commit": "0613f6f54de9247c17b4bae924197ffb4cbd2966", "diff": "commit 0613f6f54de9247c17b4bae924197ffb4cbd2966\nAuthor: retoor \nDate: Wed Mar 5 18:48:27 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 399a0a1..f8a87c8 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -39,6 +39,9 @@ class NotificationService(BaseService):\n if not channel_member['new_count']:\n channel_member['new_count'] = 0\n channel_member['new_count'] += 1\n+ \n+ usr = await self.services.user.get(uid=channel_member[\"user_uid\"])\n+ print(\"INSERTED FOR \",usr[\"username\"],flush=True)\n await self.services.channel_member.save(channel_member)\n \n model = await self.new()"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Improve notification visibility by initializing new_count", "commit": "1807cff67d3a4306f122d7df4436cc88137f299c", "diff": "commit 1807cff67d3a4306f122d7df4436cc88137f299c\nAuthor: retoor \nDate: Wed Mar 5 18:51:50 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex f8a87c8..6041901 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -32,9 +32,9 @@ class NotificationService(BaseService):\n user = await self.services.user.get(uid=channel_message[\"user_uid\"])\n async for channel_member in self.services.channel_member.find(\n channel_uid=channel_message[\"channel_uid\"],\n- is_banned=False,\n- is_muted=False,\n- deleted_at=None,\n ):\n if not channel_member['new_count']:\n channel_member['new_count'] = 0"} +{"repo": ".", "date": "2025-03-07", "line": "feat: Implemented threads view with basic message display", "commit": "5b78165fb7e83f7e8a45f790c0f6e5a9fde758a0", "diff": "commit 5b78165fb7e83f7e8a45f790c0f6e5a9fde758a0\nAuthor: retoor \nDate: Fri Mar 7 20:58:53 2025 +0100\n\n New stuff.\n\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nnew file mode 100644\nindex 0000000..2d7d01b\n--- /dev/null\n+++ b/src/snek/templates/threads.html\n@@ -0,0 +1,153 @@\n+{% extends \"app.html\" %}\n+\n+{% block main %}\n+
\n+
\n+

?

\n+
\n+
\n+ {% for thread in threads %}\n+ {% autoescape false %}\n+
\n+
\n+
\n+
{{thread.last_message_user_nick}}
\n+
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ thread.last_message_text }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{%\n+ endautoescape %}
\n+
\n+
\n+
\n+\n+ {% endautoescape %}\n+ {% endfor %}\n+
\n+ \n+
\n+\n+\n+{% endblock %}\ndiff --git a/src/snek/view/threads.py b/src/snek/view/threads.py\nnew file mode 100644\nindex 0000000..d8f4359\n--- /dev/null\n+++ b/src/snek/view/threads.py\n@@ -0,0 +1,31 @@\n+from snek.system.view import BaseView\n+\n+class ThreadsView(BaseView):\n+\n+ async def get(self):\n+ threads = []\n+ user = await self.services.user.get(uid=self.session.get(\"uid\"))\n+ async for channel_member in user.get_channel_members():\n+ thread = {}\n+ channel = await self.services.channel.get(uid=channel_member[\"channel_uid\"])\n+ thread[\"uid\"] = channel['uid']\n+ thread[\"name\"] = await channel_member.get_name()\n+ thread[\"new_count\"] = channel_member[\"new_count\"]\n+ thread[\"last_message_on\"] = channel[\"last_message_on\"]\n+ thread['created_at'] = thread['last_message_on']\n+ last_message = await channel.get_last_message()\n+ if last_message:\n+ thread[\"last_message_text\"] = last_message[\"message\"]\n+ thread['last_message_user_uid'] = last_message[\"user_uid\"]\n+ user_last_message = await self.app.services.user.get(uid=last_message[\"user_uid\"])\n+ thread['last_message_user_nick'] = user_last_message[\"nick\"]\n+ thread['last_message_user_color'] = user_last_message['color']\n+ else:\n+ thread[\"last_message_text\"] = None \n+ thread['last_message_user_uid'] = None \n+ thread['last_message_user_nick'] = None \n+ thread['last_message_user_color'] = None\n+ threads.append(thread)\n+\n+\n+ return await self.render_template(\"threads.html\", dict(threads=threads,user=user))"} +{"repo": ".", "date": "2025-03-07", "line": "feat: Added threads view and related model updates", "commit": "a1afaacb2ea6ded2eb14eb6d8fbdaf34708d9568", "diff": "commit a1afaacb2ea6ded2eb14eb6d8fbdaf34708d9568\nAuthor: retoor \nDate: Fri Mar 7 20:59:11 2025 +0100\n\n New stuff.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 6d9346d..e1f74e2 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -1,7 +1,9 @@\n import pathlib\n import asyncio\n \n-import logging \n+import logging\n+\n+from snek.view.threads import ThreadsView \n \n logging.basicConfig(level=logging.DEBUG)\n \n@@ -112,6 +114,7 @@ class Application(BaseApplication):\n self.router.add_get(\"/http-photo\", self.handle_http_photo)\n self.router.add_get(\"/rpc.ws\", RPCView)\n self.router.add_view(\"/channel/{channel}.html\", WebView)\n+ self.router.add_view(\"/threads.html\", ThreadsView)\n \n self.add_subapp(\n \"/docs\",\ndiff --git a/src/snek/model/channel.py b/src/snek/model/channel.py\nindex 8a40ced..0070948 100644\n--- a/src/snek/model/channel.py\n+++ b/src/snek/model/channel.py\n@@ -1,3 +1,4 @@\n+from snek.model.channel_message import ChannelMessageModel\n from snek.system.model import BaseModel, ModelField\n \n \n@@ -10,3 +11,11 @@ class ChannelModel(BaseModel):\n is_listed = ModelField(name=\"is_listed\", required=True, kind=bool, value=True)\n index = ModelField(name=\"index\", required=True, kind=int, value=1000)\n last_message_on = ModelField(name=\"last_message_on\", required=False, kind=str)\n+\n+ async def get_last_message(self)->ChannelMessageModel:\n+ async for model in self.app.services.channel_message.query(\"SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1\",dict(channel_uid=self['uid'])):\n+ return model \n+ return None\n+\n+ async def get_members(self):\n+ return await self.app.services.channel_member.find(channel_uid=self['uid'],deleted_at=None,is_banned=False)\n\\ No newline at end of file\ndiff --git a/src/snek/model/channel_member.py b/src/snek/model/channel_member.py\nindex 65ba3e4..5a8332e 100644\n--- a/src/snek/model/channel_member.py\n+++ b/src/snek/model/channel_member.py\n@@ -14,3 +14,27 @@ class ChannelMemberModel(BaseModel):\n is_muted = ModelField(name=\"is_muted\", required=True, kind=bool, value=False)\n is_banned = ModelField(name=\"is_banned\", required=True, kind=bool, value=False)\n new_count = ModelField(name=\"new_count\", required=False, kind=int, value=0)\n+\n+ async def get_user(self):\n+ return await self.app.services.user.get(uid=self['user_uid'])\n+ \n+ async def get_channel(self):\n+ return await self.app.services.channel.get(uid=self['channel_uid'])\n+\n+ async def get_name(self):\n+ if self[\"channel_uid\"] == \"dm\":\n+ user = await self.get_other_dm_user()\n+ return user['nick']\n+ channel = await self.get_channel()\n+ return channel['name']\n+\n+ async def get_other_dm_user(self):\n+ channel = await self.get_channel()\n+ if channel[\"tag\"] != \"dm\":\n+ return None\n+ \n+ async for model in self.app.services.channel_member.find(channel_uid=channel['uid']):\n+ if model[\"uid\"] != self['uid']:\n+ return await self.app.services.user.get(uid=model[\"user_uid\"])\n+ return await self.get_user()\n+ \n\\ No newline at end of file\ndiff --git a/src/snek/model/channel_message.py b/src/snek/model/channel_message.py\nindex 0bda0bc..4b84fdc 100644\n--- a/src/snek/model/channel_message.py\n+++ b/src/snek/model/channel_message.py\n@@ -1,3 +1,4 @@\n+from snek.model.user import UserModel\n from snek.system.model import BaseModel, ModelField\n \n \n@@ -6,3 +7,9 @@ class ChannelMessageModel(BaseModel):\n user_uid = ModelField(name=\"user_uid\", required=True, kind=str)\n message = ModelField(name=\"message\", required=True, kind=str)\n html = ModelField(name=\"html\", required=False, kind=str)\n+\n+ async def get_user(self)->UserModel:\n+ return await self.app.services.user.get(uid=self[\"user_uid\"])\n+ \n+ async def get_channel(self):\n+ return await self.app.services.channel.get(uid=self[\"channel_uid\"])\n\\ No newline at end of file\ndiff --git a/src/snek/model/user.py b/src/snek/model/user.py\nindex 54b6f30..0621ecf 100644\n--- a/src/snek/model/user.py\n+++ b/src/snek/model/user.py\n@@ -31,3 +31,7 @@ class UserModel(BaseModel):\n password = ModelField(name=\"password\", required=True, min_length=1)\n \n last_ping = ModelField(name=\"last_ping\", required=False, kind=str)\n+\n+ async def get_channel_members(self):\n+ async for channel_member in self.app.services.channel_member.find(user_uid=self['uid'],is_banned=False,deleted_at=None):\n+ yield channel_member\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 6041901..f8a87c8 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -32,9 +32,9 @@ class NotificationService(BaseService):\n user = await self.services.user.get(uid=channel_message[\"user_uid\"])\n async for channel_member in self.services.channel_member.find(\n channel_uid=channel_message[\"channel_uid\"],\n+ is_banned=False,\n+ is_muted=False,\n+ deleted_at=None,\n ):\n if not channel_member['new_count']:\n channel_member['new_count'] = 0\ndiff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py\nindex e57b0fa..e7415e1 100644\n--- a/src/snek/system/mapper.py\n+++ b/src/snek/system/mapper.py\n@@ -20,7 +20,7 @@ class BaseMapper:\n return self.app.db\n \n async def new(self):\n- return self.model_class(mapper=self)\n+ return self.model_class(mapper=self, app=self.app)\n \n @property\n def table(self):\ndiff --git a/src/snek/system/model.py b/src/snek/system/model.py\nindex ba3fc45..1aef4ae 100644\n--- a/src/snek/system/model.py\n+++ b/src/snek/system/model.py\n@@ -270,7 +270,8 @@ class BaseModel:\n return self\n \n def __init__(self, *args, **kwargs):\n- self._mapper = None\n+ self._mapper = kwargs.get(\"mapper\")\n+ self.app = kwargs.get(\"app\")\n self.fields = {}\n for key in dir(self.__class__):\n obj = getattr(self.__class__, key)\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 185d22e..60109de 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -26,7 +26,7 @@\n \ud83c\udfe0\n \ud83d\udd0d\n- \ud83d\udc65\n+ \ud83d\udc65\n \ud83d\udd12\n "} +{"repo": ".", "date": "2025-03-08", "line": "fix: Prevent race condition when reconnecting socket", "commit": "e3afc1ba6e97378688027a60d6d98cc19a519a8c", "diff": "commit e3afc1ba6e97378688027a60d6d98cc19a519a8c\nAuthor: retoor \nDate: Sat Mar 8 03:51:54 2025 +0100\n\n Fix socket issue.\n\ndiff --git a/src/snek/static/app.js b/src/snek/static/app.js\nindex b04e8fb..f26919e 100644\n--- a/src/snek/static/app.js\n+++ b/src/snek/static/app.js\n@@ -183,8 +183,9 @@ class Socket extends EventHandler {\n const me = this \n if (this.isConnected || this.isConnecting) {\n return new Promise((resolve) => {\n+ if(me.isConnected)resolve(me)\n+ else if(me.isConnecting)\n me.connectPromises.push(resolve);\n- if (!me.isConnecting) resolve(me);\n });\n }\n this.isConnecting = true;"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Improve thread display with consistent avatar styling", "commit": "37f6725f2f7e36ec03416f191c9d16cd864991ea", "diff": "commit 37f6725f2f7e36ec03416f191c9d16cd864991ea\nAuthor: retoor \nDate: Sat Mar 8 04:07:21 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nindex 2d7d01b..8131224 100644\n--- a/src/snek/templates/threads.html\n+++ b/src/snek/templates/threads.html\n@@ -11,7 +11,7 @@\n
\n-
\n
\n
{{thread.last_message_user_nick}}
"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Display user avatars in threads", "commit": "095be5892db198d0a6356c8700ed0c038e419a29", "diff": "commit 095be5892db198d0a6356c8700ed0c038e419a29\nAuthor: retoor \nDate: Sat Mar 8 04:10:05 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nindex 8131224..bb54f4d 100644\n--- a/src/snek/templates/threads.html\n+++ b/src/snek/templates/threads.html\n@@ -11,7 +11,7 @@\n
\n-
\n
\n
{{thread.last_message_user_nick}}
"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Improve thread avatar visibility", "commit": "9292e3b8f3b64084d6bcc0b13dd42d015f4799d9", "diff": "commit 9292e3b8f3b64084d6bcc0b13dd42d015f4799d9\nAuthor: retoor \nDate: Sat Mar 8 04:11:34 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nindex bb54f4d..4141e23 100644\n--- a/src/snek/templates/threads.html\n+++ b/src/snek/templates/threads.html\n@@ -11,7 +11,7 @@\n
\n-
\n
\n
{{thread.last_message_user_nick}}
"} +{"repo": ".", "date": "2025-03-08", "line": "fix: Improve thread display and opacity", "commit": "24260f9c371ab2d989441e391f513f6460eaa1ec", "diff": "commit 24260f9c371ab2d989441e391f513f6460eaa1ec\nAuthor: retoor \nDate: Sat Mar 8 04:24:39 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nindex 4141e23..8ec781f 100644\n--- a/src/snek/templates/threads.html\n+++ b/src/snek/templates/threads.html\n@@ -2,9 +2,6 @@\n \n {% block main %}\n
\n-
\n-

?

\n-
\n
\n {% for thread in threads %}\n {% autoescape false %}\n@@ -14,10 +11,10 @@\n
\n
\n-
{{thread.last_message_user_nick}}
\n+
{{thread.last_message_user_nick}}
\n
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ thread.last_message_text }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{%\n endautoescape %}
\n-
\n+
\n
\n
"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Unified styling and thread display in chat area", "commit": "1b72063a5b972dd726c647b7397f0ced16bd66c2", "diff": "commit 1b72063a5b972dd726c647b7397f0ced16bd66c2\nAuthor: retoor \nDate: Sat Mar 8 07:22:14 2025 +0100\n\n Channel list.\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex e42784b..4405785 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -102,7 +102,7 @@ a {\n overflow-y: auto;\n }\n \n-.chat-messages {\n+.chat-messages, .threads {\n flex: 1;\n overflow-y: scroll;\n scrollbar-width: none;\n@@ -130,7 +130,7 @@ a {\n display: none;\n }\n \n-.chat-messages .message {\n+.chat-messages .message, .threads .thread {\n display: flex;\n align-items: flex-start;\n margin-bottom: 0;\n@@ -138,7 +138,7 @@ a {\n border-radius: 8px;\n }\n \n-.chat-messages .message .avatar {\n+.chat-messages .message .avatar, .threads .thread .avatar {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n@@ -152,11 +152,11 @@ a {\n margin-right: 15px;\n }\n \n-.chat-messages .message .message-content {\n+.chat-messages .message .message-content, .threads .thread .message-content {\n flex: 1;\n }\n \n-.chat-messages .message .message-content .author {\n+.chat-messages .message .message-content .author, .threads .thread .message-content .author {\n font-weight: bold;\n margin-bottom: 3px;\n@@ -172,7 +172,7 @@ word-break: break-word;\n overflow-wrap: break-word;\n hyphens: auto;\n }\n-.chat-messages .message .message-content .text {\n+.chat-messages .message .message-content .text, .threads .thread .message-content .text {\n margin-bottom: 5px;\n word-break: break-word;\n@@ -191,7 +191,7 @@ hyphens: auto;\n }\n }\n \n-.chat-messages .message .message-content .time {\n+.chat-messages .message .message-content .time, .threads .thread .message-content .time {\n font-size: 0.8em;\n }\n@@ -294,7 +294,7 @@ input[type=\"text\"], .chat-input textarea {\n }\n \n-.chat-messages {\n+.chat-messages, .threads {\n scrollbar-width: none;\n }\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nindex 8ec781f..b982914 100644\n--- a/src/snek/templates/threads.html\n+++ b/src/snek/templates/threads.html\n@@ -2,12 +2,12 @@\n \n {% block main %}\n
\n-
\n+
\n {% for thread in threads %}\n {% autoescape false %}\n
\n+ class=\"thread\">\n
\n
\n@@ -32,71 +32,11 @@\n });\n }\n \n- function isElementVisible(element) {\n- const rect = element.getBoundingClientRect();\n- return (\n- rect.top >= 0 &&\n- rect.left >= 0 &&\n- rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n- rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n- );\n- }\n-\n- const messagesContainer = document.querySelector(\".chat-messages\");\n-\n- function isScrolledPastHalf() {\n- let scrollTop = messagesContainer.scrollTop;\n- let scrollableHeight = messagesContainer.scrollHeight - messagesContainer.clientHeight;\n-\n- if (scrollTop < scrollableHeight / 2) {\n- return true;\n- }\n- return false;\n- }\n \n- let isLoadingExtra = false;\n-\n- async function loadExtra() {\n- const firstMessage = messagesContainer.querySelector(\".message:first-child\");\n- if (isLoadingExtra) {\n- return;\n- }\n- if (!isScrolledPastHalf()) {\n- return;\n- }\n-\n- isLoadingExtra = true;\n-\n- const messages = await app.rpc.getMessages(channelUid, 0, firstMessage.dataset.created_at);\n-\n- messages.forEach((message) => {\n- firstMessage.insertAdjacentHTML(\"beforebegin\", message.html);\n- })\n- updateLayout(false);\n-\n- isLoadingExtra = false;\n- }\n-\n- messagesContainer.addEventListener(\"scroll\", () => {\n- loadExtra();\n- });\n+ const threadsContainer = document.querySelector(\".chat-threads\");\n \n function updateLayout(doScrollDown) {\n- const messagesContainer = document.querySelector(\".chat-messages\");\n updateTimes();\n- let previousUser = null;\n- document.querySelectorAll(\".message\").forEach((message) => {\n- if (previousUser !== message.dataset.user_uid) {\n- message.classList.add(\"switch-user\");\n- previousUser = message.dataset.user_uid;\n- } else {\n- message.classList.remove(\"switch-user\");\n- }\n- });\n- lastMessage = messagesContainer.querySelector(\".message:last-child\");\n- if (doScrollDown) {\n- lastMessage.scrollIntoView({ inline: \"nearest\" });\n- }\n }\n \n setInterval(updateTimes, 1000);\n@@ -131,20 +71,17 @@\n }\n }\n \n- const messagesContainer = document.querySelector(\".chat-messages\");\n- const lastMessage = messagesContainer.querySelector(\".message:last-child\");\n- const doScrollDownBecauseLastMessageIsVisible = !lastMessage || isElementVisible(lastMessage);\n+ const threadsContainer = document.querySelector(\".chat-threads\");\n \n const message = document.createElement(\"div\");\n message.innerHTML = data.html;\n- document.querySelector(\".chat-messages\").appendChild(message.firstChild);\n- updateLayout(doScrollDownBecauseLastMessageIsVisible);\n+ document.querySelector(\".chat-threads\").appendChild(message.firstChild);\n+ updateLayout();\n setTimeout(() => {\n- updateLayout(doScrollDownBecauseLastMessageIsVisible)\n+ updateLayout()\n }, 1000);\n });\n \n- initInputField(document.querySelector(\"textarea\"));\n updateLayout(true);\n \n {% endblock %}"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Improved thread display and DM channel naming\n\nfix: Corrected DM channel retrieval in ChannelMemberService", "commit": "5a72c8cd83d1374ff709556cbf4b4ddbf7a9ab7a", "diff": "commit 5a72c8cd83d1374ff709556cbf4b4ddbf7a9ab7a\nAuthor: retoor \nDate: Sat Mar 8 08:25:49 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/model/channel_member.py b/src/snek/model/channel_member.py\nindex 5a8332e..9689fa5 100644\n--- a/src/snek/model/channel_member.py\n+++ b/src/snek/model/channel_member.py\n@@ -22,11 +22,11 @@ class ChannelMemberModel(BaseModel):\n return await self.app.services.channel.get(uid=self['channel_uid'])\n \n async def get_name(self):\n- if self[\"channel_uid\"] == \"dm\":\n+ channel = await self.get_channel()\n+ if channel[\"tag\"] == \"dm\":\n user = await self.get_other_dm_user()\n return user['nick']\n- channel = await self.get_channel()\n- return channel['name']\n+ return channel['name'] or self['label']\n \n async def get_other_dm_user(self):\n channel = await self.get_channel()\n@@ -37,4 +37,4 @@ class ChannelMemberModel(BaseModel):\n if model[\"uid\"] != self['uid']:\n return await self.app.services.user.get(uid=model[\"user_uid\"])\n return await self.get_user()\n- \n\\ No newline at end of file\n+ \ndiff --git a/src/snek/service/channel_member.py b/src/snek/service/channel_member.py\nindex 191a063..42415d1 100644\n--- a/src/snek/service/channel_member.py\n+++ b/src/snek/service/channel_member.py\n@@ -36,7 +36,11 @@ class ChannelMemberService(BaseService):\n async def get_dm(self,from_user, to_user):\n 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)):\n return model\n- return None \n+ if not from_user == to_user:\n+ return None \n+ 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)):\n+ \n+ return model \n \n async def get_other_dm_user(self, channel_uid, user_uid):\n channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nindex b982914..73c256a 100644\n--- a/src/snek/templates/threads.html\n+++ b/src/snek/templates/threads.html\n@@ -5,18 +5,18 @@\n
\n {% for thread in threads %}\n {% autoescape false %}\n-
\n
\n
\n-
{{thread.last_message_user_nick}}
\n+
{{thread.name}}
\n
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ thread.last_message_text }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{%\n endautoescape %}
\n
\n
\n-
\n+ \n \n {% endautoescape %}\n {% endfor %}\ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex bdcddae..2c48cb4 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -3,7 +3,7 @@\n {% block main %}\n
\n
\n-

{{ channel.label.value }}

\n+

{{ name }}

\n
\n
\n {% for message in messages %}\ndiff --git a/src/snek/view/threads.py b/src/snek/view/threads.py\nindex d8f4359..772685f 100644\n--- a/src/snek/view/threads.py\n+++ b/src/snek/view/threads.py\n@@ -8,23 +8,24 @@ class ThreadsView(BaseView):\n async for channel_member in user.get_channel_members():\n thread = {}\n channel = await self.services.channel.get(uid=channel_member[\"channel_uid\"])\n+ last_message = await channel.get_last_message()\n+ if not last_message:\n+ continue\n+\n thread[\"uid\"] = channel['uid']\n thread[\"name\"] = await channel_member.get_name()\n thread[\"new_count\"] = channel_member[\"new_count\"]\n thread[\"last_message_on\"] = channel[\"last_message_on\"]\n thread['created_at'] = thread['last_message_on']\n- last_message = await channel.get_last_message()\n- if last_message:\n- thread[\"last_message_text\"] = last_message[\"message\"]\n- thread['last_message_user_uid'] = last_message[\"user_uid\"]\n- user_last_message = await self.app.services.user.get(uid=last_message[\"user_uid\"])\n- thread['last_message_user_nick'] = user_last_message[\"nick\"]\n- thread['last_message_user_color'] = user_last_message['color']\n- else:\n- thread[\"last_message_text\"] = None \n- thread['last_message_user_uid'] = None \n- thread['last_message_user_nick'] = None \n- thread['last_message_user_color'] = None\n+\n+ \n+ thread[\"last_message_text\"] = last_message[\"message\"]\n+ thread['last_message_user_uid'] = last_message[\"user_uid\"]\n+ user_last_message = await self.app.services.user.get(uid=last_message[\"user_uid\"])\n+ if channel['tag'] == \"dm\":\n+ thread['name_color'] = user_last_message['color']\n+ thread['last_message_user_color'] = user_last_message['color'] \n threads.append(thread)\n \n \ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex 8fd5ddc..3ae7902 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -42,6 +42,11 @@ class WebView(BaseView):\n return web.HTTPFound(\"/channel/{}.html\".format(channel[\"uid\"]))\n if not channel:\n return web.HTTPNotFound()\n+ \n+ channel_member = await self.app.services.channel_member.get(user_uid=self.session.get(\"uid\"), channel_uid=channel[\"uid\"])\n+ if not channel_member:\n+ return web.HTTPNotFound()\n+\n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n messages = [await self.app.services.channel_message.to_extended_dict(message) for message in await self.app.services.channel_message.offset(\n channel[\"uid\"]\n@@ -60,4 +65,6 @@ class WebView(BaseView):\n item[\"name\"] = subscribed_channel[\"label\"]\n item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n channels.append(item)\n- return await self.render_template(\"web.html\", {\"channel\": channel,\"user\": user,\"messages\": messages , \"channels\": channels})\n\\ No newline at end of file\n+\n+ name = await channel_member.get_name()\n+ return await self.render_template(\"web.html\", {\"name\": name, \"channel\": channel,\"user\": user,\"messages\": messages , \"channels\": channels})"} +{"repo": ".", "date": "2025-03-08", "line": "fix: Round follow-up messages corners", "commit": "98c2213a862b253f8f967f428b0b248bbe3a32f7", "diff": "commit 98c2213a862b253f8f967f428b0b248bbe3a32f7\nAuthor: BordedDev <>\nDate: Sat Mar 8 17:25:56 2025 +0100\n\n Fix rounded corners on follow-up messages\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex 4405785..bcdc1d1 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -1,183 +1,189 @@\n * {\n- margin: 0;\n- box-sizing: border-box;\n+ margin: 0;\n+ box-sizing: border-box;\n }\n \n .gallery {\n- padding: 50px;\n- height: auto;\n- overflow: auto;\n- flex: 1;\n+ padding: 50px;\n+ height: auto;\n+ overflow: auto;\n+ flex: 1;\n }\n \n .gallery.tile, .tile {\n- width: 100px;\n- height: 100px;\n- object-fit: cover;\n- margin: 20px 10px 20px 0;\n- border-radius: 5px;\n+ width: 100px;\n+ height: 100px;\n+ object-fit: cover;\n+ margin: 20px 10px 20px 0;\n+ border-radius: 5px;\n }\n \n body {\n- font-family: Arial, sans-serif;\n- line-height: 1.5;\n- display: flex;\n- flex-direction: column;\n- height: 100vh;\n- min-width: 100%;\n+ font-family: Arial, sans-serif;\n+ line-height: 1.5;\n+ display: flex;\n+ flex-direction: column;\n+ height: 100vh;\n+ min-width: 100%;\n }\n \n main {\n- display: flex;\n- flex: 1;\n- overflow: hidden;\n+ display: flex;\n+ flex: 1;\n+ overflow: hidden;\n }\n \n header {\n- padding: 10px 20px;\n- display: flex;\n- justify-content: space-between;\n- align-items: center;\n+ padding: 10px 20px;\n+ display: flex;\n+ justify-content: space-between;\n+ align-items: center;\n }\n \n header .logo {\n- font-size: 1.5em;\n- font-weight: bold;\n+ font-size: 1.5em;\n+ font-weight: bold;\n }\n \n header nav a {\n- text-decoration: none;\n- margin-left: 15px;\n- font-size: 1em;\n- transition: color 0.3s;\n+ text-decoration: none;\n+ margin-left: 15px;\n+ font-size: 1em;\n+ transition: color 0.3s;\n }\n \n .no-select {\n- -webkit-user-select: none; \n- -moz-user-select: none; \n- -ms-user-select: none; \n- user-select: none; \n+ -webkit-user-select: none;\n+ -moz-user-select: none;\n+ -ms-user-select: none;\n+ user-select: none;\n }\n \n header nav a:hover {\n }\n \n a {\n- font-weight: bold;\n- margin-bottom: 3px;\n+ font-weight: bold;\n+ margin-bottom: 3px;\n }\n \n .chat-area {\n- flex: 1;\n- display: flex;\n- flex-direction: column;\n- overflow: hidden;\n+ flex: 1;\n+ display: flex;\n+ flex-direction: column;\n+ overflow: hidden;\n }\n \n .chat-header {\n- padding: 10px 20px;\n- user-select: none;\n+ padding: 10px 20px;\n+ user-select: none;\n }\n \n .chat-header h2 {\n- font-size: 1.2em;\n+ font-size: 1.2em;\n \n }\n \n .message-list {\n- flex: 1;\n- height: 200px;\n- padding-bottom: 40px;\n- overflow-y: auto;\n+ flex: 1;\n+ height: 200px;\n+ padding-bottom: 40px;\n+ overflow-y: auto;\n }\n \n .chat-messages, .threads {\n- flex: 1;\n- overflow-y: scroll;\n- scrollbar-width: none;\n- -ms-overflow-style: none;\n- padding: 10px;\n- height: 200px;\n+ flex: 1;\n+ overflow-y: scroll;\n+ scrollbar-width: none;\n+ -ms-overflow-style: none;\n+ padding: 10px;\n+ height: 200px;\n }\n+\n .container {\n- flex: 1;\n- padding: 10px;\n- ul {\n- list-style: none;\n- margin: 0;\n- padding: 0;\n- }\n- a {\n+ flex: 1;\n+ padding: 10px;\n+\n+ ul {\n+ list-style: none;\n+ margin: 0;\n+ padding: 0;\n+ }\n+\n+ a {\n font-size: 20px;\n- }\n- \n+ }\n+\n }\n \n .chat-messages::-webkit-scrollbar {\n- display: none;\n+ display: none;\n }\n \n .chat-messages .message, .threads .thread {\n- display: flex;\n- align-items: flex-start;\n- margin-bottom: 0;\n- padding: 5px;\n- border-radius: 8px;\n+ display: flex;\n+ align-items: flex-start;\n+ margin-bottom: 0;\n+ padding: 5px;\n+ border-radius: 8px;\n }\n \n .chat-messages .message .avatar, .threads .thread .avatar {\n- width: 40px;\n- height: 40px;\n- border-radius: 50%;\n- font-size: 1em;\n- font-weight: bold;\n- display: flex;\n- justify-content: center;\n- align-items: center;\n- margin-right: 15px;\n+ width: 40px;\n+ height: 40px;\n+ border-radius: 50%;\n+ font-size: 1em;\n+ font-weight: bold;\n+ display: flex;\n+ justify-content: center;\n+ align-items: center;\n+ margin-right: 15px;\n }\n \n .chat-messages .message .message-content, .threads .thread .message-content {\n- flex: 1;\n+ flex: 1;\n }\n \n .chat-messages .message .message-content .author, .threads .thread .message-content .author {\n- font-weight: bold;\n- margin-bottom: 3px;\n+ font-weight: bold;\n+ margin-bottom: 3px;\n }\n+\n * {\n-word-break: break-word;\n- overflow-wrap: break-word;\n+ word-break: break-word;\n+ overflow-wrap: break-word;\n hyphens: auto;\n }\n+\n .highlight pre {\n white-space: pre-wrap;\n word-break: break-word;\n overflow-wrap: break-word;\n hyphens: auto;\n- }\n+}\n+\n .chat-messages .message .message-content .text, .threads .thread .message-content .text {\n- margin-bottom: 5px;\n- word-break: break-word;\n- overflow-wrap: break-word;\n-hyphens: auto; \n+ margin-bottom: 5px;\n+ word-break: break-word;\n+ overflow-wrap: break-word;\n+ hyphens: auto;\n }\n \n .message-content {\n@@ -187,151 +193,153 @@ hyphens: auto;\n .message-content {\n \n img, video, iframe, div {\n- max-width: 100%; \n+ max-width: 90%;\n+ border-radius: 20px;\n }\n }\n \n .chat-messages .message .message-content .time, .threads .thread .message-content .time {\n- font-size: 0.8em;\n+ font-size: 0.8em;\n }\n \n .chat-input {\n- padding: 15px;\n- display: flex;\n- align-items: center;\n+ padding: 15px;\n+ display: flex;\n+ align-items: center;\n }\n \n input[type=\"text\"], .chat-input textarea {\n- flex: 1;\n- color: white;\n- border: none;\n- padding: 10px;\n- border-radius: 5px;\n- resize: none;\n+ flex: 1;\n+ color: white;\n+ border: none;\n+ padding: 10px;\n+ border-radius: 5px;\n+ resize: none;\n }\n \n .chat-input upload-button {\n- color: white;\n- border: none;\n- padding: 10px 15px;\n- margin-left: 10px;\n- border-radius: 5px;\n- cursor: pointer;\n- font-size: 1em;\n- transition: background-color 0.3s;\n+ color: white;\n+ border: none;\n+ padding: 10px 15px;\n+ margin-left: 10px;\n+ border-radius: 5px;\n+ cursor: pointer;\n+ font-size: 1em;\n+ transition: background-color 0.3s;\n }\n \n .chat-input button:hover {\n }\n \n @media (max-width: 768px) {\n- .sidebar {\n- display: none;\n- }\n+ .sidebar {\n+ display: none;\n+ }\n }\n \n .message {\n- .text {\n- max-width: 100%;\n- word-wrap: break-word;\n- overflow-wrap: break-word;\n- hyphens: auto;\n- }\n+ .text {\n+ max-width: 100%;\n+ word-wrap: break-word;\n+ overflow-wrap: break-word;\n+ hyphens: auto;\n+ }\n \n- .avatar {\n- opacity: 0;\n- }\n+ .avatar {\n+ opacity: 0;\n+ }\n \n- .author, .time {\n- display: none;\n- }\n+ .author, .time {\n+ display: none;\n+ }\n }\n \n .message.switch-user {\n- .text img,iframe, video {\n- max-width: 90%;\n- border-radius: 20px;\n- }\n- \n- .avatar {\n- user-select: none;\n- opacity: 1;\n- }\n+ .text img, iframe, video {\n+ max-width: 90%;\n+ border-radius: 20px;\n+ }\n \n- .author {\n- display: block;\n- }\n+ .avatar {\n+ user-select: none;\n+ opacity: 1;\n+ }\n+\n+ .author {\n+ display: block;\n+ }\n }\n \n-.message:has(+ .message.switch-user), .message:last-child{ \n+.message:has(+ .message.switch-user), .message:last-child {\n .time {\n- display: block;\n-}\n+ display: block;\n+ }\n }\n \n ::-webkit-scrollbar {\n- display:none;\n+ display: none;\n }\n \n ::-webkit-scrollbar-track {\n }\n \n ::-webkit-scrollbar-thumb {\n- border-radius: 3px;\n+ border-radius: 3px;\n }\n \n ::-webkit-scrollbar-thumb:hover {\n }\n \n .chat-messages, .threads {\n- scrollbar-width: none;\n+ scrollbar-width: none;\n }\n \n a {\n- text-decoration:none\n+ text-decoration: none\n }\n+\n .sidebar {\n- width: 250px;\n- padding: 20px;\n- overflow-y: auto;\n+ width: 250px;\n+ padding: 20px;\n+ overflow-y: auto;\n }\n \n .sidebar h2 {\n- font-size: 1.2em;\n- margin-bottom: 20px;\n+ font-size: 1.2em;\n+ margin-bottom: 20px;\n }\n \n .sidebar ul {\n- list-style: none;\n+ list-style: none;\n }\n \n .sidebar ul li {\n- margin-bottom: 15px;\n+ margin-bottom: 15px;\n }\n \n .sidebar ul li a {\n- text-decoration: none;\n- font-size: 1em;\n- transition: color 0.3s;\n+ text-decoration: none;\n+ font-size: 1em;\n+ transition: color 0.3s;\n }\n \n .sidebar ul li a:hover {\n }"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Improved user search display with thread-like layout and real-time updates", "commit": "0a9b66d2f76a2c4418db7149b17729bf8a2dc811", "diff": "commit 0a9b66d2f76a2c4418db7149b17729bf8a2dc811\nAuthor: retoor \nDate: Sat Mar 8 17:33:17 2025 +0100\n\n Updated search.\n\ndiff --git a/src/snek/templates/search_user.html b/src/snek/templates/search_user.html\nindex b6a0439..842b982 100644\n--- a/src/snek/templates/search_user.html\n+++ b/src/snek/templates/search_user.html\n@@ -13,16 +13,90 @@\n \n \n \n- \n \n \n+ {% endfor %}\n+
\n+\n
\n
\n-{% endblock %}\n\\ No newline at end of file\n+\n+\n+\n+{% endblock %}\ndiff --git a/src/snek/view/search_user.py b/src/snek/view/search_user.py\nindex c28b883..347d3b2 100644\n--- a/src/snek/view/search_user.py\n+++ b/src/snek/view/search_user.py\n@@ -40,14 +40,14 @@ class SearchUserView(BaseFormView):\n users = []\n query = self.request.query.get(\"query\")\n if query:\n- users = await self.app.services.user.search(query)\n+ users = [user.record for user in await self.app.services.user.search(query)]\n \n if self.request.path.endswith(\".json\"):\n return await super().get()\n-\n- return await self.render_template(\"search_user.html\", {\"users\": users, \"query\": query or ''})\n+ current_user = await self.app.services.user.get(uid=self.session.get(\"uid\"))\n+ return await self.render_template(\"search_user.html\", {\"users\": users, \"query\": query or '','current_user': current_user})\n \n async def submit(self, form):\n if await form.is_valid:\n return {\"redirect_url\": \"/search-user.html?query=\" + form['username']}\n- return {\"is_valid\": False}\n\\ No newline at end of file\n+ return {\"is_valid\": False}"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Set default page titles and update login/register titles", "commit": "6ecd356cc08e17596ff6b5007c46def2bc17c851", "diff": "commit 6ecd356cc08e17596ff6b5007c46def2bc17c851\nAuthor: BordedDev <>\nDate: Sat Mar 8 18:04:53 2025 +0100\n\n Made improvement to page titles (mainly setting a default, login and regsiter)\n\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex 36a012c..0785366 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -1,26 +1,25 @@\n \n \n \n- \n- \n- {% block title %}{% endblock %}\n- \n- \n- \n- \n- \n+ \n+ \n+ {% block title %}Snek chat by Molodetz{% endblock %}\n+ \n+ \n+ \n+ \n+ \n \n \n- \n- \n+ \n \n \n-
\n- {% block header %}\n- {% endblock %}\n+
\n+ {% block header %}\n+ {% endblock %}\n \n-
\n-
\n+
\n+
\n {% block main %}\n {% endblock %}\n
\ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex 0a6bcc1..0ffc379 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -1,7 +1,11 @@\n {% extends \"base.html\" %}\n \n+{% block title %}\n+ Login - Snek chat by Molodetz\n+{% endblock %}\n+\n {% block main %}\n- \n- \n+ \n+ \n \n {% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex f8d1067..2b783a7 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -1,7 +1,11 @@\n {% extends \"base.html\" %}\n \n+{% block title %}\n+ Register - Snek chat by Molodetz\n+{% endblock %}\n+\n {% block main %}\n-\n- \n- \n+ \n+\n+ \n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-03-08", "line": "fix: Improved form submission and change event handling", "commit": "804556b74d8caa5e3a79a03cd1a8d7870843b898", "diff": "commit 804556b74d8caa5e3a79a03cd1a8d7870843b898\nAuthor: BordedDev <>\nDate: Sat Mar 8 18:09:14 2025 +0100\n\n Fixed issues with auto complete not working correctly with form sync\n\ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nindex 730d70a..e71d817 100644\n--- a/src/snek/static/generic-form.js\n+++ b/src/snek/static/generic-form.js\n@@ -62,7 +62,7 @@ class GenericField extends HTMLElement {\n \n constructor() {\n super();\n- this.attachShadow({ mode: 'open' });\n+ this.attachShadow({mode: 'open'});\n this.container = document.createElement('div');\n this.styleElement = document.createElement('style');\n this.styleElement.innerHTML = `\n@@ -165,18 +165,26 @@ class GenericField extends HTMLElement {\n const me = this;\n this.inputElement.addEventListener(\"keyup\", (e) => {\n if (e.key === 'Enter') {\n+ const event = new CustomEvent(\"change\", {detail: me, bubbles: true});\n+ me.dispatchEvent(event);\n+\n me.dispatchEvent(new Event(\"submit\"));\n } else if (me.field.value !== e.target.value) {\n- const event = new CustomEvent(\"change\", { detail: me, bubbles: true });\n+ const event = new CustomEvent(\"change\", {detail: me, bubbles: true});\n me.dispatchEvent(event);\n }\n });\n \n this.inputElement.addEventListener(\"click\", (e) => {\n- const event = new CustomEvent(\"click\", { detail: me, bubbles: true });\n+ const event = new CustomEvent(\"click\", {detail: me, bubbles: true});\n me.dispatchEvent(event);\n });\n \n+ this.inputElement.addEventListener(\"blur\", (e) => {\n+ const event = new CustomEvent(\"change\", {detail: me, bubbles: true});\n+ me.dispatchEvent(event);\n+ }, true);\n+\n this.container.appendChild(this.inputElement);\n }\n \n@@ -226,7 +234,7 @@ class GenericForm extends HTMLElement {\n \n constructor() {\n super();\n- this.attachShadow({ mode: 'open' });\n+ this.attachShadow({mode: 'open'});\n this.styleElement = document.createElement(\"style\");\n this.styleElement.innerHTML = `\n \n@@ -307,6 +315,16 @@ class GenericForm extends HTMLElement {\n }\n }\n });\n+\n+ fieldElement.addEventListener(\"submit\", async (e) => {\n+ const isValid = await this.validate();\n+ if (isValid) {\n+ const saveResult = await this.submit();\n+ if (saveResult.redirect_url) {\n+ window.location.pathname = saveResult.redirect_url;\n+ }\n+ }\n+ })\n });\n \n } catch (error) {\n@@ -322,7 +340,7 @@ class GenericForm extends HTMLElement {\n headers: {\n 'Content-Type': 'application/json'\n },\n- body: JSON.stringify({ \"action\": \"validate\", \"form\": this.form })\n+ body: JSON.stringify({\"action\": \"validate\", \"form\": this.form})\n });\n \n const form = await response.json();\n@@ -353,7 +371,7 @@ class GenericForm extends HTMLElement {\n headers: {\n 'Content-Type': 'application/json'\n },\n- body: JSON.stringify({ \"action\": \"submit\", \"form\": this.form })\n+ body: JSON.stringify({\"action\": \"submit\", \"form\": this.form})\n });\n return await response.json();\n }"} +{"repo": ".", "date": "2025-03-08", "line": "docs: Added meta information to base.html", "commit": "7c22a70722db3fa97a813c30c01c4cd5462138eb", "diff": "commit 7c22a70722db3fa97a813c30c01c4cd5462138eb\nAuthor: BordedDev <>\nDate: Sat Mar 8 18:25:15 2025 +0100\n\n Added additional meta info to base.html\n\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex 0785366..1e89c50 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -3,7 +3,15 @@\n \n \n \n+ \n+ \n+ \n+ \n+ \n+\n {% block title %}Snek chat by Molodetz{% endblock %}\n+\n \n \n "} +{"repo": ".", "date": "2025-03-08", "line": "feat: Sort threads by last message timestamp", "commit": "8e195a49e3e914a4b241e95378bd9a07611715a8", "diff": "commit 8e195a49e3e914a4b241e95378bd9a07611715a8\nAuthor: retoor \nDate: Sat Mar 8 18:41:37 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/view/threads.py b/src/snek/view/threads.py\nindex 772685f..14431f1 100644\n--- a/src/snek/view/threads.py\n+++ b/src/snek/view/threads.py\n@@ -27,6 +27,7 @@ class ThreadsView(BaseView):\n thread['name_color'] = user_last_message['color']\n thread['last_message_user_color'] = user_last_message['color'] \n threads.append(thread)\n-\n+ \n+ threads.sort(key=lambda x: x['last_message_on'], reverse=True)\n \n return await self.render_template(\"threads.html\", dict(threads=threads,user=user))"} +{"repo": ".", "date": "2025-03-08", "line": "style: Improved form input focus and placeholder styling", "commit": "c9113ca09500c5b3cc277fb09b9607a505d39f30", "diff": "commit c9113ca09500c5b3cc277fb09b9607a505d39f30\nAuthor: BordedDev <>\nDate: Sat Mar 8 18:56:55 2025 +0100\n\n Tweaked form input styling\n\ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nindex e71d817..11647dc 100644\n--- a/src/snek/static/generic-form.js\n+++ b/src/snek/static/generic-form.js\n@@ -75,16 +75,28 @@ class GenericField extends HTMLElement {\n }\n \n input {\n- width: 90%;\n- padding: 10px;\n- margin: 10px 0;\n- border-radius: 5px;\n- font-size: 1em;\n+ width: 90%;\n+ padding: 10px;\n+ margin: 10px 0;\n+ border-radius: 5px;\n+ font-size: 1em;\n+ \n+ &:focus {\n+ }\n+ \n+ &::placeholder {\n+ transition: opacity 0.3s;\n+ }\n+ \n+ &:focus::placeholder {\n+ opacity: 0.4;\n+ }\n }\n-\n+ \n button {\n width: 50%;\n padding: 10px;"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Added head block for custom HTML head content", "commit": "62aa15a4b4d6514824378cca73084c9ce2df903b", "diff": "commit 62aa15a4b4d6514824378cca73084c9ce2df903b\nAuthor: BordedDev <>\nDate: Sat Mar 8 18:59:00 2025 +0100\n\n Added head block\n\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex 1e89c50..d93b568 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -20,6 +20,9 @@\n \n \n \n+\n+ {% block head %}\n+ {% endblock %}\n \n \n
"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Implemented back button styling and layout", "commit": "ad7eab9717848584369ded6e19babb6a7b9f5b98", "diff": "commit ad7eab9717848584369ded6e19babb6a7b9f5b98\nAuthor: BordedDev <>\nDate: Sat Mar 8 19:07:09 2025 +0100\n\n Tweaked the back button position\n\ndiff --git a/src/snek/static/back-form.css b/src/snek/static/back-form.css\nnew file mode 100644\nindex 0000000..4824d6c\n--- /dev/null\n+++ b/src/snek/static/back-form.css\n@@ -0,0 +1,18 @@\n+.back-form {\n+ display: grid;\n+ grid-template-columns: auto auto;\n+ grid-template-rows: auto auto;\n+\n+ fancy-button {\n+ grid-column: 1 / 1;\n+ grid-row: 1 / 1;\n+ z-index: 1;\n+ margin-left: 30px;\n+ margin-top: 30px;\n+ }\n+\n+ generic-form {\n+ grid-column: 1 / 3;\n+ grid-row: 1 / 3;\n+ }\n+}\n\\ No newline at end of file\ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex 0ffc379..ed81224 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -4,8 +4,13 @@\n Login - Snek chat by Molodetz\n {% endblock %}\n \n-{% block main %}\n- \n- \n+{% block head %}\n+ \n+{% endblock %}\n \n+{% block main %}\n+
\n+ \n+ \n+
\n {% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex 2b783a7..2fa89d3 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -4,8 +4,14 @@\n Register - Snek chat by Molodetz\n {% endblock %}\n \n+{% block head %}\n+ \n+{% endblock %}\n+\n {% block main %}\n- \n+
\n+ \n \n- \n+ \n+
\n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Revert whitespace changes", "commit": "e91ae4584aaf08cb02b89cdf26b8c43a8c8179b0", "diff": "commit e91ae4584aaf08cb02b89cdf26b8c43a8c8179b0\nAuthor: BordedDev <>\nDate: Sat Mar 8 19:35:22 2025 +0100\n\n Undo auto formatting\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex bcdc1d1..3d154a9 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -1,345 +1,345 @@\n * {\n- margin: 0;\n- box-sizing: border-box;\n+ margin: 0;\n+ box-sizing: border-box;\n }\n \n .gallery {\n- padding: 50px;\n- height: auto;\n- overflow: auto;\n- flex: 1;\n+ padding: 50px;\n+ height: auto;\n+ overflow: auto;\n+ flex: 1;\n }\n \n .gallery.tile, .tile {\n- width: 100px;\n- height: 100px;\n- object-fit: cover;\n- margin: 20px 10px 20px 0;\n- border-radius: 5px;\n+ width: 100px;\n+ height: 100px;\n+ object-fit: cover;\n+ margin: 20px 10px 20px 0;\n+ border-radius: 5px;\n }\n \n body {\n- font-family: Arial, sans-serif;\n- line-height: 1.5;\n- display: flex;\n- flex-direction: column;\n- height: 100vh;\n- min-width: 100%;\n+ font-family: Arial, sans-serif;\n+ line-height: 1.5;\n+ display: flex;\n+ flex-direction: column;\n+ height: 100vh;\n+ min-width: 100%;\n }\n \n main {\n- display: flex;\n- flex: 1;\n- overflow: hidden;\n+ display: flex;\n+ flex: 1;\n+ overflow: hidden;\n }\n \n header {\n- padding: 10px 20px;\n- display: flex;\n- justify-content: space-between;\n- align-items: center;\n+ padding: 10px 20px;\n+ display: flex;\n+ justify-content: space-between;\n+ align-items: center;\n }\n \n header .logo {\n- font-size: 1.5em;\n- font-weight: bold;\n+ font-size: 1.5em;\n+ font-weight: bold;\n }\n \n header nav a {\n- text-decoration: none;\n- margin-left: 15px;\n- font-size: 1em;\n- transition: color 0.3s;\n+ text-decoration: none;\n+ margin-left: 15px;\n+ font-size: 1em;\n+ transition: color 0.3s;\n }\n \n .no-select {\n- -webkit-user-select: none;\n- -moz-user-select: none;\n- -ms-user-select: none;\n- user-select: none;\n+ -webkit-user-select: none;\n+ -moz-user-select: none;\n+ -ms-user-select: none;\n+ user-select: none;\n }\n \n header nav a:hover {\n }\n \n a {\n- font-weight: bold;\n- margin-bottom: 3px;\n+ font-weight: bold;\n+ margin-bottom: 3px;\n }\n \n .chat-area {\n- flex: 1;\n- display: flex;\n- flex-direction: column;\n- overflow: hidden;\n+ flex: 1;\n+ display: flex;\n+ flex-direction: column;\n+ overflow: hidden;\n }\n \n .chat-header {\n- padding: 10px 20px;\n- user-select: none;\n+ padding: 10px 20px;\n+ user-select: none;\n }\n \n .chat-header h2 {\n- font-size: 1.2em;\n+ font-size: 1.2em;\n \n }\n \n .message-list {\n- flex: 1;\n- height: 200px;\n- padding-bottom: 40px;\n- overflow-y: auto;\n+ flex: 1;\n+ height: 200px;\n+ padding-bottom: 40px;\n+ overflow-y: auto;\n }\n \n .chat-messages, .threads {\n- flex: 1;\n- overflow-y: scroll;\n- scrollbar-width: none;\n- -ms-overflow-style: none;\n- padding: 10px;\n- height: 200px;\n+ flex: 1;\n+ overflow-y: scroll;\n+ scrollbar-width: none;\n+ -ms-overflow-style: none;\n+ padding: 10px;\n+ height: 200px;\n }\n \n .container {\n- flex: 1;\n- padding: 10px;\n+ flex: 1;\n+ padding: 10px;\n \n- ul {\n- list-style: none;\n- margin: 0;\n- padding: 0;\n- }\n+ ul {\n+ list-style: none;\n+ margin: 0;\n+ padding: 0;\n+ }\n \n- a {\n- font-size: 20px;\n- }\n+ a {\n+ font-size: 20px;\n+ }\n \n }\n \n .chat-messages::-webkit-scrollbar {\n- display: none;\n+ display: none;\n }\n \n .chat-messages .message, .threads .thread {\n- display: flex;\n- align-items: flex-start;\n- margin-bottom: 0;\n- padding: 5px;\n- border-radius: 8px;\n+ display: flex;\n+ align-items: flex-start;\n+ margin-bottom: 0;\n+ padding: 5px;\n+ border-radius: 8px;\n }\n \n .chat-messages .message .avatar, .threads .thread .avatar {\n- width: 40px;\n- height: 40px;\n- border-radius: 50%;\n- font-size: 1em;\n- font-weight: bold;\n- display: flex;\n- justify-content: center;\n- align-items: center;\n- margin-right: 15px;\n+ width: 40px;\n+ height: 40px;\n+ border-radius: 50%;\n+ font-size: 1em;\n+ font-weight: bold;\n+ display: flex;\n+ justify-content: center;\n+ align-items: center;\n+ margin-right: 15px;\n }\n \n .chat-messages .message .message-content, .threads .thread .message-content {\n- flex: 1;\n+ flex: 1;\n }\n \n .chat-messages .message .message-content .author, .threads .thread .message-content .author {\n- font-weight: bold;\n- margin-bottom: 3px;\n+ font-weight: bold;\n+ margin-bottom: 3px;\n }\n \n * {\n- word-break: break-word;\n- overflow-wrap: break-word;\n- hyphens: auto;\n+ word-break: break-word;\n+ overflow-wrap: break-word;\n+ hyphens: auto;\n }\n \n .highlight pre {\n- white-space: pre-wrap;\n- word-break: break-word;\n- overflow-wrap: break-word;\n- hyphens: auto;\n+ white-space: pre-wrap;\n+ word-break: break-word;\n+ overflow-wrap: break-word;\n+ hyphens: auto;\n }\n \n .chat-messages .message .message-content .text, .threads .thread .message-content .text {\n- margin-bottom: 5px;\n- word-break: break-word;\n- overflow-wrap: break-word;\n- hyphens: auto;\n+ margin-bottom: 5px;\n+ word-break: break-word;\n+ overflow-wrap: break-word;\n+ hyphens: auto;\n }\n \n .message-content {\n- max-width: 100%;\n+ max-width: 100%;\n }\n \n .message-content {\n \n- img, video, iframe, div {\n- max-width: 90%;\n- border-radius: 20px;\n- }\n+ img, video, iframe, div {\n+ max-width: 90%;\n+ border-radius: 20px;\n+ }\n }\n \n .chat-messages .message .message-content .time, .threads .thread .message-content .time {\n- font-size: 0.8em;\n+ font-size: 0.8em;\n }\n \n .chat-input {\n- padding: 15px;\n- display: flex;\n- align-items: center;\n+ padding: 15px;\n+ display: flex;\n+ align-items: center;\n }\n \n input[type=\"text\"], .chat-input textarea {\n- flex: 1;\n- color: white;\n- border: none;\n- padding: 10px;\n- border-radius: 5px;\n- resize: none;\n+ flex: 1;\n+ color: white;\n+ border: none;\n+ padding: 10px;\n+ border-radius: 5px;\n+ resize: none;\n }\n \n .chat-input upload-button {\n- color: white;\n- border: none;\n- padding: 10px 15px;\n- margin-left: 10px;\n- border-radius: 5px;\n- cursor: pointer;\n- font-size: 1em;\n- transition: background-color 0.3s;\n+ color: white;\n+ border: none;\n+ padding: 10px 15px;\n+ margin-left: 10px;\n+ border-radius: 5px;\n+ cursor: pointer;\n+ font-size: 1em;\n+ transition: background-color 0.3s;\n }\n \n .chat-input button:hover {\n }\n \n @media (max-width: 768px) {\n- .sidebar {\n- display: none;\n- }\n+ .sidebar {\n+ display: none;\n+ }\n }\n \n .message {\n- .text {\n- max-width: 100%;\n- word-wrap: break-word;\n- overflow-wrap: break-word;\n- hyphens: auto;\n- }\n+ .text {\n+ max-width: 100%;\n+ word-wrap: break-word;\n+ overflow-wrap: break-word;\n+ hyphens: auto;\n+ }\n \n- .avatar {\n- opacity: 0;\n- }\n+ .avatar {\n+ opacity: 0;\n+ }\n \n- .author, .time {\n- display: none;\n- }\n+ .author, .time {\n+ display: none;\n+ }\n }\n \n .message.switch-user {\n- .text img, iframe, video {\n- max-width: 90%;\n- border-radius: 20px;\n- }\n+ .text img, iframe, video {\n+ max-width: 90%;\n+ border-radius: 20px;\n+ }\n \n- .avatar {\n- user-select: none;\n- opacity: 1;\n- }\n+ .avatar {\n+ user-select: none;\n+ opacity: 1;\n+ }\n \n- .author {\n- display: block;\n- }\n+ .author {\n+ display: block;\n+ }\n }\n \n .message:has(+ .message.switch-user), .message:last-child {\n- .time {\n- display: block;\n- }\n+ .time {\n+ display: block;\n+ }\n }\n \n ::-webkit-scrollbar {\n- display: none;\n+ display: none;\n }\n \n ::-webkit-scrollbar-track {\n }\n \n ::-webkit-scrollbar-thumb {\n- border-radius: 3px;\n+ border-radius: 3px;\n }\n \n ::-webkit-scrollbar-thumb:hover {\n }\n \n .chat-messages, .threads {\n- scrollbar-width: none;\n+ scrollbar-width: none;\n }\n \n a {\n- text-decoration: none\n+ text-decoration: none\n }\n \n .sidebar {\n- width: 250px;\n- padding: 20px;\n- overflow-y: auto;\n+ width: 250px;\n+ padding: 20px;\n+ overflow-y: auto;\n }\n \n .sidebar h2 {\n- font-size: 1.2em;\n- margin-bottom: 20px;\n+ font-size: 1.2em;\n+ margin-bottom: 20px;\n }\n \n .sidebar ul {\n- list-style: none;\n+ list-style: none;\n }\n \n .sidebar ul li {\n- margin-bottom: 15px;\n+ margin-bottom: 15px;\n }\n \n .sidebar ul li a {\n- text-decoration: none;\n- font-size: 1em;\n- transition: color 0.3s;\n+ text-decoration: none;\n+ font-size: 1em;\n+ transition: color 0.3s;\n }\n \n .sidebar ul li a:hover {\n }"} +{"repo": ".", "date": "2025-03-08", "line": "Merge main", "commit": "dd5a9a23e8e452a03f7080656608617309bf73a5", "diff": "commit dd5a9a23e8e452a03f7080656608617309bf73a5\nMerge: e91ae45 8e195a4\nAuthor: BordedDev \nDate: Sat Mar 8 18:40:43 2025 +0000\n\n Merge branch 'main' into main"} +{"repo": ".", "date": "2025-03-08", "line": "Merge pull request #123\n", "commit": "aedfe9aa947dcd2262c825af5a4d977eb298ccb5", "diff": "commit aedfe9aa947dcd2262c825af5a4d977eb298ccb5\nMerge: 8e195a4 dd5a9a2\nAuthor: retoor \nDate: Sat Mar 8 18:53:02 2025 +0000\n\n \n Reviewed-by: retoor "} +{"repo": ".", "date": "2025-03-08", "line": "feat: Sort channels by last message time", "commit": "a219ce4d79a15ef900583ab025fb0da1df79ace3", "diff": "commit a219ce4d79a15ef900583ab025fb0da1df79ace3\nAuthor: retoor \nDate: Sat Mar 8 20:21:02 2025 +0100\n\n Update sorting.\n\ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex 3ae7902..be44df9 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -58,6 +58,9 @@ class WebView(BaseView):\n async for subscribed_channel in self.app.services.channel_member.find(user_uid=self.session.get(\"uid\"), deleted_at=None, is_banned=False):\n item = {}\n other_user = await self.app.services.channel_member.get_other_dm_user(subscribed_channel[\"channel_uid\"], self.session.get(\"uid\"))\n+ parent_object = await subscribed_channel.get_channel()\n+ last_message =await parent_object.get_last_message()\n+ item[\"last_message_on\"] = parent_object[\"last_message_on\"]\n if other_user:\n item[\"name\"] = other_user[\"nick\"]\n item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n@@ -65,6 +68,8 @@ class WebView(BaseView):\n item[\"name\"] = subscribed_channel[\"label\"]\n item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n channels.append(item)\n+ \n+ channels.sort(key=lambda x: x['last_message_on'], reverse=True)\n \n name = await channel_member.get_name()\n return await self.render_template(\"web.html\", {\"name\": name, \"channel\": channel,\"user\": user,\"messages\": messages , \"channels\": channels})"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Separate public and private channels in sidebar", "commit": "d6061cb45b68a7b393b0e35a560d5d7bea4b9478", "diff": "commit d6061cb45b68a7b393b0e35a560d5d7bea4b9478\nAuthor: retoor \nDate: Sat Mar 8 20:27:05 2025 +0100\n\n Changes.\n\ndiff --git a/src/snek/templates/sidebar_channels.html b/src/snek/templates/sidebar_channels.html\nindex ed16ffd..9dd20b5 100644\n--- a/src/snek/templates/sidebar_channels.html\n+++ b/src/snek/templates/sidebar_channels.html\n@@ -6,7 +6,13 @@\n
\n-\n \n \n-\n {% block head %}\n {% endblock %}\n \n@@ -35,4 +35,4 @@\n {% endblock %}\n
\n \n-\n\\ No newline at end of file\n+\ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nindex 1894bc4..ffbd5bc 100644\n--- a/src/snek/templates/index.html\n+++ b/src/snek/templates/index.html\n@@ -7,6 +7,7 @@\n \n \n \n \n \n
"} +{"repo": ".", "date": "2025-03-17", "line": "fix: Scroll to the end of the message container", "commit": "54416ee84f88064897a824ae2c3a9e0ef2c1ccaa", "diff": "commit 54416ee84f88064897a824ae2c3a9e0ef2c1ccaa\nAuthor: retoor \nDate: Mon Mar 17 09:15:49 2025 +0100\n\n Added block end.\n\ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex e1e443b..932a1fa 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -138,7 +138,7 @@\n });\n lastMessage = messagesContainer.querySelector(\".message:last-child\");\n if (doScrollDown) { \n- lastMessage?.scrollIntoView({ inline: \"nearest\" });\n+ lastMessage?.scrollIntoView({ block: \"end\", inline: \"nearest\" });\n }\n }"} +{"repo": ".", "date": "2025-03-17", "line": "feat: Refactor header layout and logo display", "commit": "39fa8fa0cd7a725edc1f293a7e8d0ea0d5d5bca6", "diff": "commit 39fa8fa0cd7a725edc1f293a7e8d0ea0d5d5bca6\nAuthor: retoor \nDate: Mon Mar 17 17:01:01 2025 +0100\n\n Layout update.\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex 943e819..0b475d1 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -39,16 +39,19 @@ main {\n \n header {\n- padding: 10px 20px;\n+ padding-top: 10px;\n+ padding-left: 20px;\n+ padding-right: 20px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n \n header .logo {\n- font-size: 1.5em;\n font-weight: bold;\n+ font-size: 1.2em;\n }\n \n header nav a {\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 31f5d7f..a8bb473 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -21,7 +21,9 @@\n \n \n
\n-
Snek
\n+
Snek
\n+ \n+
{% block header_text %}{% endblock %}
\n \n+\n
\n
\n {% block sidebar %}\ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex 932a1fa..8a68f64 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -1,10 +1,9 @@\n {% extends \"app.html\" %}\n \n+{% block header_text %}

{{ name }}

{% endblock %} \n+\n {% block main %}\n
\n-
\n-

{{ name }}

\n-
\n
\n {% for message in messages %}\n {% autoescape false %}"} +{"repo": ".", "date": "2025-03-17", "line": "feat: Add Promise.withResolvers polyfill and update templates", "commit": "7fa2817f773b47737236f4bb700023bcf8b483f1", "diff": "commit 7fa2817f773b47737236f4bb700023bcf8b483f1\nAuthor: BordedDev <>\nDate: Mon Mar 17 22:05:46 2025 +0100\n\n Added Promise.withResolvers pollyfill\n\ndiff --git a/src/snek/static/polyfills/Promise.withResolvers.js b/src/snek/static/polyfills/Promise.withResolvers.js\nnew file mode 100644\nindex 0000000..03f0185\n--- /dev/null\n+++ b/src/snek/static/polyfills/Promise.withResolvers.js\n@@ -0,0 +1,8 @@\n+Promise.withResolvers = Promise.withResolvers || function() {\n+ let resolve, reject;\n+ let promise = new Promise((res, rej) => {\n+ resolve = res;\n+ reject = rej;\n+ });\n+ return { promise, resolve, reject };\n+}\n\\ No newline at end of file\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 31f5d7f..2d96495 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -6,6 +6,7 @@\n \n Snek\n \n+ \n \ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex df0796c..d5df6eb 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -12,7 +12,8 @@\n \n {% block title %}Snek chat by Molodetz{% endblock %}\n \n- \n+ \n+ \n \n \n \n@@ -20,7 +21,8 @@\n \n \n \n+ data-website-id=\"d127c3e4-dc70-4041-a1c8-bcc32c2492ea\">\n {% block head %}\n {% endblock %}\n "} +{"repo": ".", "date": "2025-03-17", "line": "revert: Removed unnecessary website ID attribute", "commit": "965dc930a900a5080e225bb492be2b799daed22f", "diff": "commit 965dc930a900a5080e225bb492be2b799daed22f\nAuthor: BordedDev <>\nDate: Mon Mar 17 22:06:52 2025 +0100\n\n Undid formatting\n\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex d5df6eb..d2f8de7 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -21,8 +21,7 @@\n \n \n \n- data-website-id=\"d127c3e4-dc70-4041-a1c8-bcc32c2492ea\">\n {% block head %}\n {% endblock %}\n "} +{"repo": ".", "date": "2025-03-17", "line": "Merge: Improved code formatting and added review notes.", "commit": "825ece4e7868be28ba03c4fde5149695b0dd9dc5", "diff": "commit 825ece4e7868be28ba03c4fde5149695b0dd9dc5\nMerge: 39fa8fa 965dc93\nAuthor: retoor \nDate: Mon Mar 17 21:09:55 2025 +0000\n\n \n Reviewed-by: retoor "} +{"repo": ".", "date": "2025-03-18", "line": "feat: Added dump script for public channels", "commit": "3c6a0944d68ca16250ec9364d7f006b0e7eea6e8", "diff": "commit 3c6a0944d68ca16250ec9364d7f006b0e7eea6e8\nAuthor: retoor \nDate: Tue Mar 18 00:46:32 2025 +0100\n\n Added dump script.\n\ndiff --git a/Makefile b/Makefile\nindex e3febf7..70b06a0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -8,6 +8,9 @@ PORT = 8081\n python:\n \t$(PYTHON)\n \n+dump:\n+\t$(PYTHON) -m snek.dump\n+\n run:\n \t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\n \t\ndiff --git a/src/snek/dump.py b/src/snek/dump.py\nnew file mode 100644\nindex 0000000..6401637\n--- /dev/null\n+++ b/src/snek/dump.py\n@@ -0,0 +1,17 @@\n+import json \n+\n+from snek.app import app\n+\n+\n+def dump_public_channels():\n+ result = {'channels':{}}\n+\n+ for channel in app.db['channel'].find(is_private=False,is_listed=True):\n+ result['channels'][channel['label']] = dict(channel)\n+ result['channels'][channel['label']]['messages'] = list(dict(record) for record in app.db['channel_message'].find(channel_uid=channel['uid']))\n+\n+ print(json.dumps(result, sort_keys=True, indent=4,default=str),end='',flush=True)\n+ \n+\n+if __name__ == '__main__':\n+ dump_public_channels()"} +{"repo": ".", "date": "2025-03-18", "line": "feat: Improved dump functionality with JSON output and user information\n\n", "commit": "70db15bf27c7a8fd8bf112432f02754f01bbb3d7", "diff": "commit 70db15bf27c7a8fd8bf112432f02754f01bbb3d7\nAuthor: retoor \nDate: Tue Mar 18 00:13:25 2025 +0000\n\n Update.\n\ndiff --git a/Makefile b/Makefile\nindex 70b06a0..62d24ca 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -9,7 +9,7 @@ python:\n \t$(PYTHON)\n \n dump:\n-\t$(PYTHON) -m snek.dump\n+\t@$(PYTHON) -m snek.dump\n \n run:\n \t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\ndiff --git a/src/snek/dump.py b/src/snek/dump.py\nindex 6401637..cf3d417 100644\n--- a/src/snek/dump.py\n+++ b/src/snek/dump.py\n@@ -1,17 +1,33 @@\n+import asyncio\n import json \n \n from snek.app import app\n \n+async def fix_message(message):\n+ message = dict(\n+ uid=message['uid'],\n+ user_uid=message['user_uid'],\n+ text=message['message'],\n+ sent=message['created_at']\n+ )\n+ user = await app.services.user.get(uid=message['user_uid'])\n+ message['user'] = user and user['username'] or None\n+ return message\n \n-def dump_public_channels():\n+async def dump_public_channels():\n result = {'channels':{}}\n-\n- for channel in app.db['channel'].find(is_private=False,is_listed=True):\n+ for channel in app.db['channel'].find(is_private=False,is_listed=True,tag='public'):\n+ print(f\"Dumping channel: {channel['label']}.\")\n result['channels'][channel['label']] = dict(channel)\n- result['channels'][channel['label']]['messages'] = list(dict(record) for record in app.db['channel_message'].find(channel_uid=channel['uid']))\n-\n- print(json.dumps(result, sort_keys=True, indent=4,default=str),end='',flush=True)\n+ result['channels'][channel['label']]['messages'] = [await fix_message(record) for record in app.db['channel_message'].find(channel_uid=channel['uid'],order_by='created_at')]\n+ print(\"Dump succesfull!\")\n+ print(\"Converting to json.\")\n+ data = json.dumps(result, indent=4,default=str)\n+ print(\"Converting succesful, now writing to dump.json\")\n+ with open(\"dump.json\",\"w\") as f:\n+ f.write(data)\n+ print(\"Dump written to dump.json\")\n \n \n if __name__ == '__main__':\n- dump_public_channels()\n+ asyncio.run(dump_public_channels())\ndiff --git a/src/snek/system/cache.py b/src/snek/system/cache.py\nindex f9c4761..cd9484d 100644\n--- a/src/snek/system/cache.py\n+++ b/src/snek/system/cache.py\n@@ -20,13 +20,13 @@ class Cache:\n try:\n self.lru.pop(self.lru.index(args))\n except:\n- print(\"Cache miss!\", args, flush=True)\n return None\n self.lru.insert(0, args)\n while len(self.lru) > self.max_items:\n self.cache.pop(self.lru[-1])\n self.lru.pop()\n- print(\"Cache hit!\", args, flush=True)\n return self.cache[args]\n \n def json_default(self, value):\n@@ -61,7 +61,7 @@ class Cache:\n \n if is_new:\n self.version += 1\n- print(f\"Cache store! {len(self.lru)} items. New version:\", self.version, flush=True)\n \n async def delete(self, args):\n if args in self.cache:"} +{"repo": ".", "date": "2025-03-18", "line": "feat: Refactor dump script to output to text file", "commit": "3960390ec45f427979ebd2b81c1a21666a47e71d", "diff": "commit 3960390ec45f427979ebd2b81c1a21666a47e71d\nAuthor: retoor \nDate: Tue Mar 18 21:06:28 2025 +0000\n\n Updated dump script.\n\ndiff --git a/src/snek/dump.py b/src/snek/dump.py\nindex cf3d417..1b7eb6b 100644\n--- a/src/snek/dump.py\n+++ b/src/snek/dump.py\n@@ -12,20 +12,18 @@ async def fix_message(message):\n )\n user = await app.services.user.get(uid=message['user_uid'])\n message['user'] = user and user['username'] or None\n- return message\n+ return (message['user'] or '') + ': ' + (message['text'] or '')\n \n async def dump_public_channels():\n- result = {'channels':{}}\n+ result = []\n for channel in app.db['channel'].find(is_private=False,is_listed=True,tag='public'):\n print(f\"Dumping channel: {channel['label']}.\")\n- result['channels'][channel['label']] = dict(channel)\n- result['channels'][channel['label']]['messages'] = [await fix_message(record) for record in app.db['channel_message'].find(channel_uid=channel['uid'],order_by='created_at')]\n+ result += [await fix_message(record) for record in app.db['channel_message'].find(channel_uid=channel['uid'],order_by='created_at')]\n print(\"Dump succesfull!\")\n print(\"Converting to json.\")\n- data = json.dumps(result, indent=4,default=str)\n print(\"Converting succesful, now writing to dump.json\")\n- with open(\"dump.json\",\"w\") as f:\n- f.write(data)\n+ with open(\"dump.txt\",\"w\") as f:\n+ f.write('\\n\\n'.join(result))\n print(\"Dump written to dump.json\")"} +{"repo": ".", "date": "2025-03-20", "line": "refactor: Update session management for user registration", "commit": "5ba239caa8928a078362af0e6e2d1a4626bd508d", "diff": "commit 5ba239caa8928a078362af0e6e2d1a4626bd508d\nAuthor: retoor \nDate: Thu Mar 20 02:12:00 2025 +0100\n\n Changes by r.\n\ndiff --git a/src/snek/view/login.py b/src/snek/view/login.py\nindex 6d6d6ad..5028a7a 100644\n--- a/src/snek/view/login.py\n+++ b/src/snek/view/login.py\n@@ -31,4 +31,4 @@ class LoginView(BaseFormView):\n \"color\": user[\"color\"]\n })\n return {\"redirect_url\": \"/web.html\"}\n- return {\"is_valid\": False}\n+ return {\"is_valid\": False}\n\\ No newline at end of file\ndiff --git a/src/snek/view/register.py b/src/snek/view/register.py\nindex db812b5..6e49506 100644\n--- a/src/snek/view/register.py\n+++ b/src/snek/view/register.py\n@@ -24,8 +24,10 @@ class RegisterView(BaseFormView):\n result = await self.app.services.user.register(\n form.email.value, form.username.value, form.password.value\n )\n- self.request.session[\"uid\"] = result[\"uid\"]\n- self.request.session[\"username\"] = result[\"username\"]\n- self.request.session[\"logged_in\"] = True\n- self.request.session[\"color\"] = result[\"color\"]\n+ self.request.session.update({\n+ \"uid\": result[\"uid\"],\n+ \"username\": result[\"username\"],\n+ \"logged_in\": True,\n+ \"color\": result[\"color\"]\n+ })\n return {\"redirect_url\": \"/web.html\"}\n\\ No newline at end of file\ndiff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py\nindex 858edad..5ce42a7 100644\n--- a/src/snek/view/register_form.py\n+++ b/src/snek/view/register_form.py\n@@ -14,21 +14,20 @@\n \n from snek.form.register import RegisterForm\n from snek.system.view import BaseFormView\n \n-\n class RegisterFormView(BaseFormView):\n form = RegisterForm\n \n@@ -36,8 +35,10 @@ class RegisterFormView(BaseFormView):\n result = await self.app.services.user.register(\n form.email.value, form.username.value, form.password.value\n )\n- self.request.session[\"uid\"] = result[\"uid\"]\n- self.request.session[\"username\"] = result[\"username\"]\n- self.request.session[\"logged_in\"] = True\n-\n+ self.request.session.update({\n+ \"uid\": result[\"uid\"],\n+ \"username\": result[\"username\"],\n+ \"logged_in\": True,\n+ \"color\": result[\"color\"]\n+ })\n return {\"redirect_url\": \"/web.html\"}\n\\ No newline at end of file\ndiff --git a/src/snek/view/status.py b/src/snek/view/status.py\nindex 9428f08..117942a 100644\n--- a/src/snek/view/status.py\n+++ b/src/snek/view/status.py\n@@ -19,11 +19,10 @@\n \n-\n from snek.system.view import BaseView\n \n class StatusView(BaseView):"} +{"repo": ".", "date": "2025-03-22", "line": "feat: Added terminal access and updated Dockerfile and compose.yml", "commit": "7dcabde2ed0ab699ea3033f7788381e85c352b97", "diff": "commit 7dcabde2ed0ab699ea3033f7788381e85c352b97\nAuthor: retoor \nDate: Sat Mar 22 18:16:36 2025 +0100\n\n Update.\n\ndiff --git a/Dockerfile b/Dockerfile\nindex ffdc3d7..6ef9f83 100644\n--- a/Dockerfile\n+++ b/Dockerfile\n@@ -13,6 +13,7 @@ RUN apk add --no-cache \\\n libxext \\\n libssl3 \\\n ca-certificates \\\n+ docker \\\n fontconfig \\\n freetype \\\n ttf-dejavu \\\ndiff --git a/compose.yml b/compose.yml\nindex 17bcf22..70d21ba 100644\n--- a/compose.yml\n+++ b/compose.yml\n@@ -2,10 +2,14 @@ services:\n snek:\n build: .\n restart: always\n+ privileged: true\n ports:\n - \"8081:8081\"\n volumes:\n - ./:/code\n+ - /media/storage/snek/molodetz.nl/drive:/code/drive\n+ - /var/run/docker.sock:/var/run/docker.sock\n+ - /media/storage/snek/molodetz.nl/drive:/drive\n environment:\n - PYTHONDONTWRITEBYTECODE=1\n - PYTHONUNBUFFERED=1\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex e1f74e2..a7bc195 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -37,7 +37,7 @@ from snek.view.upload import UploadView\n from snek.view.search_user import SearchUserView \n from snek.view.avatar import AvatarView\n from snek.system.profiler import profiler_handler\n-\n+from snek.view.terminal import TerminalView, TerminalSocketView\n \n SESSION_KEY = b\"c79a0c5fda4b424189c427d28c9f7c34\"\n \n@@ -115,6 +115,8 @@ class Application(BaseApplication):\n self.router.add_get(\"/rpc.ws\", RPCView)\n self.router.add_view(\"/channel/{channel}.html\", WebView)\n self.router.add_view(\"/threads.html\", ThreadsView)\n+ self.router.add_view(\"/terminal.ws\", TerminalSocketView)\n+ self.router.add_view(\"/terminal.html\", TerminalView)\n \n self.add_subapp(\n \"/docs\","} +{"repo": ".", "date": "2025-03-22", "line": "feat: Added terminal access and basic shell environment.", "commit": "013d4adce57f4afec5176bbdb5e2225d529ec3b7", "diff": "commit 013d4adce57f4afec5176bbdb5e2225d529ec3b7\nAuthor: retoor \nDate: Sat Mar 22 19:57:39 2025 +0100\n\n Drive access.\n\ndiff --git a/Dockerfile b/Dockerfile\nindex 6ef9f83..d24c47d 100644\n--- a/Dockerfile\n+++ b/Dockerfile\n@@ -1,41 +1,10 @@\n-FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf \n-FROM python:3.12.8-alpine3.21\n+FROM python:3.14.0a6-bookworm\n+RUN mkdir -p /code \n WORKDIR /code\n-ENV FLASK_APP=app.py\n-ENV FLASK_RUN_HOST=0.0.0.0\n-RUN apk add --no-cache gcc musl-dev linux-headers git\n-\n-RUN apk add --no-cache \\\n- libstdc++ \\\n- libx11 \\\n- libxrender \\\n- libxext \\\n- libssl3 \\\n- ca-certificates \\\n- docker \\\n- fontconfig \\\n- freetype \\\n- ttf-dejavu \\\n- ttf-droid \\\n- ttf-freefont \\\n- ttf-liberation \\\n- && apk add --no-cache --virtual .build-deps \\\n- msttcorefonts-installer \\\n- && update-ms-fonts \\\n- && fc-cache -f \\\n- && apk del .build-deps\n-COPY --from=wkhtmltopdf /bin/wkhtmltopdf /bin/wkhtmltopdf\n-COPY --from=wkhtmltopdf /bin/wkhtmltoimage /bin/wkhtmltoimage\n+RUN apt update && apt install build-essential docker -y \n COPY pyproject.toml pyproject.toml \n COPY src src\n+RUN mkdir /drive\n RUN pip install --upgrade pip\n RUN pip install -e .\n-EXPOSE 8081\n \n-CMD [\"python\",\"-m\",\"snek.app\"]\ndiff --git a/compose.yml b/compose.yml\nindex 70d21ba..9d55d3b 100644\n--- a/compose.yml\n+++ b/compose.yml\n@@ -7,26 +7,12 @@ services:\n - \"8081:8081\"\n volumes:\n - ./:/code\n- - /media/storage/snek/molodetz.nl/drive:/code/drive\n+ - /media/storage/snek.molodetz.nl/drive:/code/drive\n+ - /media/storage/snek.molodetz.nl/drive:/drive\n - /var/run/docker.sock:/var/run/docker.sock\n- - /media/storage/snek/molodetz.nl/drive:/drive\n environment:\n - PYTHONDONTWRITEBYTECODE=1\n - PYTHONUNBUFFERED=1\n entrypoint: [\"gunicorn\", \"-w\", \"1\", \"-k\", \"aiohttp.worker.GunicornWebWorker\", \"snek.gunicorn:app\",\"--bind\",\"0.0.0.0:8081\"]\n- snecssh:\n- build:\n- context: .\n- dockerfile: DockerfileDrive\n- restart: always\n- ports:\n- - \"2225:2225\"\n- volumes:\n- - ./:/code\n- environment:\n- - PYTHONDONTWRITEBYTECODE=\"1\"\n- - PYTHONUNBUFFERED=\"1\"\n- entrypoint: [\"python\",\"-m\",\"snekssh.app2\"]\n \ndiff --git a/src/snek/scripts/chat.js b/src/snek/scripts/chat.js\nnew file mode 100644\nindex 0000000..a4d6782\n--- /dev/null\n+++ b/src/snek/scripts/chat.js\n@@ -0,0 +1,54 @@\n+const channelUid = \"{{ channel.uid.value }}\";\n+\n+function initInputField(textBox) {\n+ textBox.addEventListener('change', (e) => {\n+ e.preventDefault();\n+ this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));\n+ });\n+\n+ textBox.addEventListener('keydown', (e) => {\n+ if (e.key === 'Enter' && !e.shiftKey) {\n+ e.preventDefault();\n+ const message = e.target.value.trim();\n+ if (message) {\n+ app.rpc.sendMessage(channelUid, message);\n+ e.target.value = '';\n+ }\n+ }\n+ });\n+ textBox.focus();\n+}\n+\n+function updateTimes() {\n+ document.querySelectorAll(\".time\").forEach((time) => {\n+ time.innerText = app.timeDescription(time.dataset.created_at);\n+ });\n+}\n+\n+function isElementVisible(element) {\n+ const rect = element.getBoundingClientRect();\n+ return (\n+ rect.top >= 0 &&\n+ rect.left >= 0 &&\n+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n+ rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n+ );\n+}\n+\n+const messagesContainer = document.querySelector(\".chat-messages\");\n+\n+let isLoadingExtra = false;\n+\n+messagesContainer.addEventListener(\"scroll\", () => {\n+ loadExtra();\n+});\n+\n+setInterval(updateTimes, 1000);\n+\n+app.addEventListener(\"channel-message\", (data) => {\n+ if (data.channel_uid !== channelUid) {\n+ if(!isMentionForSomeoneElse(data.message)){\n+ channelSidebar.notify(data);\n+ }\n+ }\n+});\ndiff --git a/src/snek/system/terminal.py b/src/snek/system/terminal.py\nnew file mode 100644\nindex 0000000..2d9341e\n--- /dev/null\n+++ b/src/snek/system/terminal.py\n@@ -0,0 +1,48 @@\n+import asyncio\n+import aiohttp\n+import aiohttp.web\n+import os\n+import pty\n+import shlex\n+import subprocess\n+import pathlib\n+\n+commands = {\n+ 'alpine': 'docker run -it alpine /bin/sh',\n+ 'r': 'docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh',\n+}\n+\n+class TerminalSession:\n+ def __init__(self,command):\n+ self.master, self.slave = pty.openpty()\n+ self.sockets =[]\n+ self.process = subprocess.Popen(\n+ command.split(\" \"),\n+ stdin=self.slave,\n+ stdout=self.slave,\n+ stderr=self.slave,\n+ bufsize=0,\n+ universal_newlines=True\n+ )\n+\n+ async def read_output(self, ws):\n+ loop = asyncio.get_event_loop()\n+ self.sockets.append(ws)\n+ if len(self.sockets) > 1:\n+ return \n+ while True:\n+ try:\n+ data = await loop.run_in_executor(None, os.read, self.master, 1024)\n+ if not data:\n+ break\n+ try:\n+ except:\n+ self.sockets.remove(ws)\n+ except Exception:\n+ break\n+\n+ async def write_input(self, data):\n+ os.write(self.master, data.encode())\n+\n+\ndiff --git a/src/snek/templates/static b/src/snek/templates/static\nnew file mode 120000\nindex 0000000..d9bc54d\n--- /dev/null\n+++ b/src/snek/templates/static\n@@ -0,0 +1 @@\n+../static/\n\\ No newline at end of file\ndiff --git a/src/snek/templates/terminal.html b/src/snek/templates/terminal.html\nnew file mode 100644\nindex 0000000..d9ad6d1\n--- /dev/null\n+++ b/src/snek/templates/terminal.html\n@@ -0,0 +1,47 @@\n+{% extends \"app.html\" %}\n+\n+{% block sidebar %}\n+Reboot\n+{% endblock %}\n+\n+{% block main %}\n+ \n+\n+
\n+\n+ \n+\n+\n+\n+{% endblock main %}\ndiff --git a/src/snek/view/terminal.py b/src/snek/view/terminal.py\nnew file mode 100644\nindex 0000000..e54dfb7\n--- /dev/null\n+++ b/src/snek/view/terminal.py\n@@ -0,0 +1,56 @@\n+from snek.system.view import BaseView \n+import aiohttp \n+import asyncio\n+from snek.system.terminal import TerminalSession\n+import pathlib\n+\n+class TerminalSocketView(BaseView):\n+ \n+ login_required = True\n+\n+ user_sessions = {}\n+ \n+ async def prepare_drive(self):\n+ user = await self.services.user.get(uid=self.session.get(\"uid\"))\n+ root = pathlib.Path(\"drive\").joinpath(user[\"uid\"])\n+ root.mkdir(parents=True, exist_ok=True)\n+ terminal_folder = pathlib.Path(\"terminal\")\n+ for path in terminal_folder.iterdir():\n+ destination_path = root.joinpath(path.name)\n+ if not destination_path.exists():\n+ if not path.is_dir():\n+ destination_path.write_bytes(path.read_bytes())\n+ return root \n+ \n+ async def get(self):\n+ \n+\n+\n+ ws = aiohttp.web.WebSocketResponse()\n+ await ws.prepare(self.request)\n+ user = await self.services.user.get(uid=self.session.get(\"uid\"))\n+ root = await self.prepare_drive()\n+ \n+ command = f\"docker run -v ./{root}/:/root --rm -it --memory 512M --cpus=0.5 -w /root ubuntu:latest /bin/bash\"\n+ print(command)\n+\n+ session = self.user_sessions.get(user[\"uid\"])\n+ if not session:\n+ self.user_sessions[user[\"uid\"]] = TerminalSession(command=command)\n+ session = self.user_sessions[user[\"uid\"]] \n+ asyncio.create_task(session.read_output(ws)) \n+\n+ async for msg in ws:\n+ if msg.type == aiohttp.WSMsgType.BINARY:\n+ await session.write_input(msg.data.decode())\n+\n+ \n+ return ws\n+\n+class TerminalView(BaseView):\n+\n+ login_required = True\n+\n+ async def get(self):\n+ request = self.request\n+ return await self.request.app.render_template('terminal.html',self.request)\ndiff --git a/terminal/.bashrc b/terminal/.bashrc\nnew file mode 100644\nindex 0000000..2b5d791\n--- /dev/null\n+++ b/terminal/.bashrc\n@@ -0,0 +1,108 @@\n+\n+[ -z \"$PS1\" ] && return\n+\n+HISTCONTROL=ignoredups:ignorespace\n+\n+shopt -s histappend\n+\n+HISTSIZE=1000\n+HISTFILESIZE=2000\n+\n+shopt -s checkwinsize\n+\n+[ -x /usr/bin/lesspipe ] && eval \"$(SHELL=/bin/sh lesspipe)\"\n+\n+if [ -z \"$debian_chroot\" ] && [ -r /etc/debian_chroot ]; then\n+ debian_chroot=$(cat /etc/debian_chroot)\n+fi\n+\n+case \"$TERM\" in\n+ xterm-color) color_prompt=yes;;\n+esac\n+\n+\n+if [ -n \"$force_color_prompt\" ]; then\n+ if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then\n+\tcolor_prompt=yes\n+ else\n+\tcolor_prompt=\n+ fi\n+fi\n+\n+if [ \"$color_prompt\" = yes ]; then\n+ PS1='${debian_chroot:+($debian_chroot)}\\[\\033[01;32m\\]\\u@\\h\\[\\033[00m\\]:\\[\\033[01;34m\\]\\w\\[\\033[00m\\]\\$ '\n+else\n+ PS1='${debian_chroot:+($debian_chroot)}\\u@\\h:\\w\\$ '\n+fi\n+unset color_prompt force_color_prompt\n+\n+case \"$TERM\" in\n+xterm*|rxvt*)\n+ PS1=\"\\[\\e]0;${debian_chroot:+($debian_chroot)}\\u@\\h: \\w\\a\\]$PS1\"\n+ ;;\n+*)\n+ ;;\n+esac\n+\n+if [ -x /usr/bin/dircolors ]; then\n+ test -r ~/.dircolors && eval \"$(dircolors -b ~/.dircolors)\" || eval \"$(dircolors -b)\"\n+ alias ls='ls --color=auto'\n+\n+ alias grep='grep --color=auto'\n+ alias fgrep='fgrep --color=auto'\n+ alias egrep='egrep --color=auto'\n+fi\n+\n+alias ll='ls -alF'\n+alias la='ls -A'\n+alias l='ls -CF'\n+\n+\n+if [ -f ~/.bash_aliases ]; then\n+ . ~/.bash_aliases\n+fi\n+\n+\n+cp ~/r /usr/local/bin \n+\n+\n+apt update && apt install libreadline-dev libcurl4-openssl-dev libssl-dev libncurses5-dev libncursesw5-dev libsqlite3-dev libreadline6-dev zlib1g-dev libbz2-dev libffi-dev liblzma-dev python3 python3-pip python3-venv -y\n+\n+echo \"r is installed.\"\n+\ndiff --git a/terminal/.profile b/terminal/.profile\nnew file mode 100644\nindex 0000000..c4c7402\n--- /dev/null\n+++ b/terminal/.profile\n@@ -0,0 +1,9 @@\n+\n+if [ \"$BASH\" ]; then\n+ if [ -f ~/.bashrc ]; then\n+ . ~/.bashrc\n+ fi\n+fi\n+\n+mesg n 2> /dev/null || true\ndiff --git a/terminal/r b/terminal/r\nnew file mode 100755\nindex 0000000..2cc65df\nBinary files /dev/null and b/terminal/r differ"} +{"repo": ".", "date": "2025-03-22", "line": "feat: Added terminal page with Ubuntu option and channel sidebar", "commit": "6e68408ddfd8be2376f7453deac8f63a6bfb93e4", "diff": "commit 6e68408ddfd8be2376f7453deac8f63a6bfb93e4\nAuthor: retoor \nDate: Sat Mar 22 20:04:20 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/templates/sidebar_channels.html b/src/snek/templates/sidebar_channels.html\nindex 9dd20b5..2d034b7 100644\n--- a/src/snek/templates/sidebar_channels.html\n+++ b/src/snek/templates/sidebar_channels.html\n@@ -4,6 +4,11 @@\n }\n \n \n \ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex 55accc7..b5c3504 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -90,13 +90,20 @@ class RPCView(BaseView):\n channels = []\n async for subscription in self.services.channel_member.find(user_uid=self.user_uid, is_banned=False):\n channel = await self.services.channel.get(uid=subscription['channel_uid'])\n+ last_message = await channel.get_last_message()\n+ color = None \n+ if last_message:\n+ last_message_user = await last_message.get_user()\n+ color = last_message_user['color']\n channels.append({\n \"name\": subscription[\"label\"],\n \"uid\": subscription[\"channel_uid\"],\n \"tag\": channel[\"tag\"],\n \"new_count\": subscription[\"new_count\"],\n \"is_moderator\": subscription[\"is_moderator\"],\n- \"is_read_only\": subscription[\"is_read_only\"]\n+ \"is_read_only\": subscription[\"is_read_only\"],\n+ 'new_count': subscription['new_count'],\n+ 'color': color \n })\n return channels\n \n@@ -156,9 +163,11 @@ class RPCView(BaseView):\n if result != \"noresponse\":\n await self._send_json({\"callId\": call_id, \"success\": success, \"data\": result})\n except Exception as ex:\n+ print(str(ex), flush=True)\n await self._send_json({\"callId\": call_id, \"success\": False, \"data\": str(ex)})\n \n async def _send_json(self, obj):\n+ print(obj, flush=True)\n await self.ws.send_str(json.dumps(obj, default=str))\n \n \ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex cdde6e3..b9271c3 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -46,6 +46,9 @@ class WebView(BaseView):\n channel_member = await self.app.services.channel_member.get(user_uid=self.session.get(\"uid\"), channel_uid=channel[\"uid\"])\n if not channel_member:\n return web.HTTPNotFound()\n+ \n+ channel_member['new_count'] = 0\n+ await self.app.services.channel_member.save(channel_member)\n \n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n messages = [await self.app.services.channel_message.to_extended_dict(message) for message in await self.app.services.channel_message.offset(\n@@ -54,23 +57,5 @@ class WebView(BaseView):\n for message in messages:\n await self.app.services.notification.mark_as_read(self.session.get(\"uid\"),message[\"uid\"])\n \n- channels = []\n- async for subscribed_channel in self.app.services.channel_member.find(user_uid=self.session.get(\"uid\"), deleted_at=None, is_banned=False):\n- item = {}\n- other_user = await self.app.services.channel_member.get_other_dm_user(subscribed_channel[\"channel_uid\"], self.session.get(\"uid\"))\n- parent_object = await subscribed_channel.get_channel()\n- last_message =await parent_object.get_last_message()\n- item[\"last_message_on\"] = parent_object[\"last_message_on\"]\n- item[\"is_private\"] = parent_object[\"tag\"] == \"dm\"\n- if other_user:\n- item[\"name\"] = other_user[\"nick\"]\n- item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n- else:\n- item[\"name\"] = subscribed_channel[\"label\"]\n- item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n- channels.append(item)\n- \n- channels.sort(key=lambda x: x['last_message_on'] or '', reverse=True)\n-\n name = await channel_member.get_name()\n- return await self.render_template(\"web.html\", {\"name\": name, \"channel\": channel,\"user\": user,\"messages\": messages , \"channels\": channels})\n+ return await self.render_template(\"web.html\", {\"name\": name, \"channel\": channel,\"user\": user,\"messages\": messages})"} +{"repo": ".", "date": "2025-03-27", "line": "fix: Prevent errors when user data is missing during notification updates", "commit": "877ef7970d5b7bb5f8fd0af35adad5d8d071b14d", "diff": "commit 877ef7970d5b7bb5f8fd0af35adad5d8d071b14d\nAuthor: retoor \nDate: Thu Mar 27 17:15:39 2025 +0100\n\n Bugfix.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex a5d4ebd..e6915bf 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -41,6 +41,8 @@ class NotificationService(BaseService):\n channel_member['new_count'] += 1\n \n usr = await self.services.user.get(uid=channel_member[\"user_uid\"])\n+ if not usr:\n+ continue\n print(\"INSERTED FOR \",usr[\"username\"],flush=True)\n await self.services.channel_member.save(channel_member)"} +{"repo": ".", "date": "2025-03-27", "line": "refactor: Remove debugging prints", "commit": "87c189b3fe897cc15ee6ac311e987bf9fe7811b9", "diff": "commit 87c189b3fe897cc15ee6ac311e987bf9fe7811b9\nAuthor: retoor \nDate: Thu Mar 27 17:28:54 2025 +0100\n\n Removed useless prints.\n\ndiff --git a/src/snek/service/channel_member.py b/src/snek/service/channel_member.py\nindex 42415d1..5c6c7ee 100644\n--- a/src/snek/service/channel_member.py\n+++ b/src/snek/service/channel_member.py\n@@ -5,6 +5,13 @@ class ChannelMemberService(BaseService):\n \n mapper_name = \"channel_member\"\n \n+ async def mark_as_read(self, channel_uid, user_uid):\n+ channel_member = await self.get(\n+ channel_uid=channel_uid,\n+ user_uid=user_uid\n+ )\n+ channel_member[\"new_count\"] = 0\n+ return await self.save(channel_member)\n \n async def create(\n self,\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex e6915bf..0b54a23 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -43,7 +43,6 @@ class NotificationService(BaseService):\n usr = await self.services.user.get(uid=channel_member[\"user_uid\"])\n if not usr:\n continue\n- print(\"INSERTED FOR \",usr[\"username\"],flush=True)\n await self.services.channel_member.save(channel_member)\n \n model = await self.new()\ndiff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py\nindex e7415e1..3b6c7c6 100644\n--- a/src/snek/system/mapper.py\n+++ b/src/snek/system/mapper.py\n@@ -49,7 +49,6 @@ class BaseMapper:\n if not model.record.get(\"uid\"):\n raise Exception(f\"Attempt to save without uid: {model.record}.\")\n model.updated_at.update()\n- print(model.record,flush=True)\n return self.table.upsert(model.record, [\"uid\"])\n \n async def find(self, **kwargs) -> typing.AsyncGenerator:\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nindex 5371d33..3d52892 100644\n--- a/src/snek/system/view.py\n+++ b/src/snek/system/view.py\n@@ -70,4 +70,3 @@ class BaseFormView(BaseView):\n return await self.json_response(result)\n \n async def submit(self, model=None):\n- print(\"Submit sucess\")\ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex 8a68f64..28ad4d2 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -184,6 +184,7 @@\n setTimeout(() => {\n updateLayout(doScrollDownBecauseLastMessageIsVisible)\n }, 1000);\n+ app.rpc.markAsRead(channelUid);\n });\n \n initInputField(getInputField());\ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex b5c3504..27a8dcf 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -39,9 +39,9 @@ class RPCView(BaseView):\n def is_logged_in(self):\n return self.view.session.get(\"logged_in\", False)\n \n- async def mark_as_read(self, message_uid):\n+ async def mark_as_read(self, channel_uid):\n self._require_login()\n- await self.services.notification.mark_as_read(self.user_uid, message_uid)\n+ await self.services.channel_member.mark_as_read(channel_uid, self.user_uid) \n return True\n \n async def login(self, username, password):\n@@ -167,7 +167,6 @@ class RPCView(BaseView):\n await self._send_json({\"callId\": call_id, \"success\": False, \"data\": str(ex)})\n \n async def _send_json(self, obj):\n- print(obj, flush=True)\n await self.ws.send_str(json.dumps(obj, default=str))\n \n \ndiff --git a/src/snek/view/terminal.py b/src/snek/view/terminal.py\nindex 26de464..fac680c 100644\n--- a/src/snek/view/terminal.py\n+++ b/src/snek/view/terminal.py\n@@ -31,7 +31,6 @@ class TerminalSocketView(BaseView):\n root = await self.prepare_drive()\n \n command = f\"docker run -v ./{root}/:/root --rm -it --memory 512M --cpus=0.5 -w /root ubuntu:latest /bin/bash\"\n- print(command)\n \n session = self.user_sessions.get(user[\"uid\"])\n if not session:"} +{"repo": ".", "date": "2025-03-27", "line": "refactor: Removed unnecessary print statements", "commit": "71a206a19fa6b2a2ae05d886a39d2fa03f2c0f71", "diff": "commit 71a206a19fa6b2a2ae05d886a39d2fa03f2c0f71\nAuthor: retoor \nDate: Thu Mar 27 17:30:06 2025 +0100\n\n Removed useless prints.\n\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nindex 3d52892..2765642 100644\n--- a/src/snek/system/view.py\n+++ b/src/snek/system/view.py\n@@ -70,3 +70,4 @@ class BaseFormView(BaseView):\n return await self.json_response(result)\n \n async def submit(self, model=None):\n+ pass"} +{"repo": ".", "date": "2025-03-27", "line": "feat: Added database transaction for notification creation", "commit": "bd5bb5ae65d6cb870d090382b106b705833d4cf1", "diff": "commit bd5bb5ae65d6cb870d090382b106b705833d4cf1\nAuthor: retoor \nDate: Thu Mar 27 20:10:05 2025 +0100\n\n Transaction.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 0b54a23..1392044 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -30,6 +30,7 @@ class NotificationService(BaseService):\n uid=channel_message_uid\n )\n user = await self.services.user.get(uid=channel_message[\"user_uid\"])\n+ self.app.db.begin()\n async for channel_member in self.services.channel_member.find(\n channel_uid=channel_message[\"channel_uid\"],\n is_banned=False,\n@@ -56,3 +57,5 @@ class NotificationService(BaseService):\n await self.save(model)\n except Exception as ex:\n raise Exception(f\"Failed to create notification: {model.errors}.\")\n+\n+ self.app.db.commit()"} +{"repo": ".", "date": "2025-03-27", "line": "feat: Persist RPC transaction changes to database", "commit": "13ce09a5c50ddba8956dbeb838f9dd1bcafe184a", "diff": "commit 13ce09a5c50ddba8956dbeb838f9dd1bcafe184a\nAuthor: retoor \nDate: Thu Mar 27 20:20:48 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex 27a8dcf..5720f04 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -203,7 +203,9 @@ class RPCView(BaseView):\n if msg.type == web.WSMsgType.TEXT:\n try:\n async with Profiler():\n+ self.app.db.begin()\n await rpc(msg.json())\n+ self.app.db.commit()\n except Exception as ex:\n print(ex, flush=True)\n await self.services.socket.delete(ws)"} +{"repo": ".", "date": "2025-03-27", "line": "feat: Added task runner and task queue for asynchronous operations", "commit": "145373399dada2f4cee54af0c99e62d4a27a0f99", "diff": "commit 145373399dada2f4cee54af0c99e62d4a27a0f99\nAuthor: retoor \nDate: Thu Mar 27 20:41:04 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 1d225cc..3094dd5 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -61,6 +61,7 @@ class Application(BaseApplication):\n middlewares=middlewares, template_path=self.template_path, *args, **kwargs\n )\n session_setup(self, EncryptedCookieStorage(SESSION_KEY))\n+ self.tasks = []\n self._middlewares.append(session_middleware)\n self.jinja2_env.add_extension(MarkdownExtension)\n self.jinja2_env.add_extension(LinkifyExtension)\n@@ -68,12 +69,28 @@ class Application(BaseApplication):\n self.jinja2_env.add_extension(EmojiExtension)\n \n self.setup_router()\n- \n+ \n self.cache = Cache(self)\n self.services = get_services(app=self)\n self.mappers = get_mappers(app=self)\n self.on_startup.append(self.prepare_database)\n \n+ async def create_task(self, task):\n+ self.tasks.append(task)\n+\n+ async def task_runner(self):\n+ while True:\n+ await asyncio.sleep(0.1)\n+ task = None\n+ try:\n+ task = self.tasks.pop(0)\n+ except IndexError:\n+ continue \n+ try:\n+ await task\n+ except:\n+ print(ex)\n+\n async def prepare_database(self,app):\n self.db.query(\"PRAGMA journal_mode=WAL\")\n self.db.query(\"PRAGMA syncnorm=off\")\n@@ -89,6 +106,7 @@ class Application(BaseApplication):\n pass \n \n await app.services.drive.prepare_all()\n+ self.loop.create_task(self.task_runner())\n \n def setup_router(self):\n self.router.add_get(\"/\", IndexView)\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex 17c677b..ecaedb7 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -21,7 +21,10 @@ class ChatService(BaseService):\n \n user = await self.services.user.get(uid=user_uid)\n await self.services.notification.create_channel_message(channel_message_uid)\n- sent_to_count = await self.services.socket.broadcast(channel_uid, dict(\n+ channel['last_message_on'] = now()\n+ await self.services.channel.save(channel)\n+ \n+ await self.app.create_task(self.services.socket.broadcast(channel_uid, dict(\n message=channel_message[\"message\"],\n html=channel_message[\"html\"],\n user_uid=user_uid,\n@@ -32,7 +35,5 @@ class ChatService(BaseService):\n username=user['username'],\n uid=channel_message['uid'],\n user_nick=user['nick']\n- ))\n- channel['last_message_on'] = now()\n- await self.services.channel.save(channel)\n- return sent_to_count\n\\ No newline at end of file\n+ )))\n+ return True"} +{"repo": ".", "date": "2025-03-27", "line": "refactor: Improve task management with asyncio.Queue and error handling", "commit": "e6f702a6b405f1dae0ce719177c6f7bf5b636ad8", "diff": "commit e6f702a6b405f1dae0ce719177c6f7bf5b636ad8\nAuthor: retoor \nDate: Thu Mar 27 20:56:35 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 3094dd5..7c0f0b0 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -61,7 +61,7 @@ class Application(BaseApplication):\n middlewares=middlewares, template_path=self.template_path, *args, **kwargs\n )\n session_setup(self, EncryptedCookieStorage(SESSION_KEY))\n- self.tasks = []\n+ self.tasks = asyncio.Queue()\n self._middlewares.append(session_middleware)\n self.jinja2_env.add_extension(MarkdownExtension)\n self.jinja2_env.add_extension(LinkifyExtension)\n@@ -76,19 +76,14 @@ class Application(BaseApplication):\n self.on_startup.append(self.prepare_database)\n \n async def create_task(self, task):\n- self.tasks.append(task)\n+ await self.tasks.put(task)\n \n async def task_runner(self):\n while True:\n- await asyncio.sleep(0.1)\n- task = None\n- try:\n- task = self.tasks.pop(0)\n- except IndexError:\n- continue \n+ task = await self.tasks.get() \n try:\n await task\n- except:\n+ except Exception as ex:\n print(ex)\n \n async def prepare_database(self,app):\ndiff --git a/src/snek/service/socket.py b/src/snek/service/socket.py\nindex 0b6071d..d941321 100644\n--- a/src/snek/service/socket.py\n+++ b/src/snek/service/socket.py\n@@ -62,12 +62,12 @@ class SocketService(BaseService):\n async def broadcast(self, channel_uid, message):\n count = 0\n async for channel_member in self.app.services.channel_member.find(channel_uid=channel_uid):\n- count += await self.send_to_user(channel_member[\"user_uid\"],message)\n- return count\n+ await self.send_to_user(channel_member[\"user_uid\"],message)\n+ return True \n \n async def delete(self, ws):\n for s in [sock for sock in self.sockets if sock.ws == ws]:\n await s.close()\n self.sockets.remove(s)\n \n- \n\\ No newline at end of file\n+"} +{"repo": ".", "date": "2025-03-27", "line": "fix: Remove unnecessary database transaction block in RPCView", "commit": "9d5815ed1028354ac61f2d506530147a02c476e6", "diff": "commit 9d5815ed1028354ac61f2d506530147a02c476e6\nAuthor: retoor \nDate: Thu Mar 27 21:00:34 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex 5720f04..27a8dcf 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -203,9 +203,7 @@ class RPCView(BaseView):\n if msg.type == web.WSMsgType.TEXT:\n try:\n async with Profiler():\n- self.app.db.begin()\n await rpc(msg.json())\n- self.app.db.commit()\n except Exception as ex:\n print(ex, flush=True)\n await self.services.socket.delete(ws)"} +{"repo": ".", "date": "2025-03-27", "line": "feat: Added database transaction management for task execution", "commit": "8810679fd8ea2dd6e63b09bf9a4b9af5c2d0fdd2", "diff": "commit 8810679fd8ea2dd6e63b09bf9a4b9af5c2d0fdd2\nAuthor: retoor \nDate: Thu Mar 27 21:01:17 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 7c0f0b0..67beb5d 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -81,10 +81,12 @@ class Application(BaseApplication):\n async def task_runner(self):\n while True:\n task = await self.tasks.get() \n+ self.db.begin()\n try:\n await task\n except Exception as ex:\n print(ex)\n+ self.db.commit()\n \n async def prepare_database(self,app):\n self.db.query(\"PRAGMA journal_mode=WAL\")"} +{"repo": ".", "date": "2025-03-27", "line": "feat: Added task execution time measurement", "commit": "6ad3844f037735e268b42247fcc6e8605cc13f07", "diff": "commit 6ad3844f037735e268b42247fcc6e8605cc13f07\nAuthor: retoor \nDate: Thu Mar 27 21:07:04 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 67beb5d..dd3e0f3 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -2,6 +2,7 @@ import pathlib\n import asyncio\n \n import logging\n+import time\n \n from snek.view.threads import ThreadsView \n \n@@ -83,7 +84,11 @@ class Application(BaseApplication):\n task = await self.tasks.get() \n self.db.begin()\n try:\n+ task_start = time.time()\n await task\n+ task_end = time.time()\n+ print(f\"Task {task} took {task_end - task_start} seconds\")\n+ self.tasks.task_done()\n except Exception as ex:\n print(ex)\n self.db.commit()"} +{"repo": ".", "date": "2025-03-27", "line": "feat: Asynchronously create channel messages and improve socket broadcast", "commit": "73e8779bdc42ec5f5618fad3d563544d1fad2b69", "diff": "commit 73e8779bdc42ec5f5618fad3d563544d1fad2b69\nAuthor: retoor \nDate: Thu Mar 27 21:11:02 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex ecaedb7..a008c70 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -20,11 +20,11 @@ class ChatService(BaseService):\n \n \n user = await self.services.user.get(uid=user_uid)\n- await self.services.notification.create_channel_message(channel_message_uid)\n+ async self.app.create_task(self.services.notification.create_channel_message(channel_message_uid))\n channel['last_message_on'] = now()\n await self.services.channel.save(channel)\n \n- await self.app.create_task(self.services.socket.broadcast(channel_uid, dict(\n+ self.services.socket.broadcast(channel_uid, dict(\n message=channel_message[\"message\"],\n html=channel_message[\"html\"],\n user_uid=user_uid,\n@@ -35,5 +35,5 @@ class ChatService(BaseService):\n username=user['username'],\n uid=channel_message['uid'],\n user_nick=user['nick']\n- )))\n+ ))\n return True"} +{"repo": ".", "date": "2025-03-27", "line": "fix: Ensure notification creation after socket broadcast", "commit": "6f043d21390d13d37772c8ae145d7a66b3919529", "diff": "commit 6f043d21390d13d37772c8ae145d7a66b3919529\nAuthor: retoor \nDate: Thu Mar 27 21:12:19 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex a008c70..aef0cdd 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -20,11 +20,10 @@ class ChatService(BaseService):\n \n \n user = await self.services.user.get(uid=user_uid)\n- async self.app.create_task(self.services.notification.create_channel_message(channel_message_uid))\n channel['last_message_on'] = now()\n await self.services.channel.save(channel)\n- \n- self.services.socket.broadcast(channel_uid, dict(\n+ \n+ await self.services.socket.broadcast(channel_uid, dict(\n message=channel_message[\"message\"],\n html=channel_message[\"html\"],\n user_uid=user_uid,\n@@ -36,4 +35,6 @@ class ChatService(BaseService):\n uid=channel_message['uid'],\n user_nick=user['nick']\n ))\n+ async self.app.create_task(self.services.notification.create_channel_message(channel_message_uid))\n+ \n return True"} +{"repo": ".", "date": "2025-03-27", "line": "fix: Await task creation in chat service", "commit": "9e50b2a6d3570c4f86fcfca6a5ce59947c0105ec", "diff": "commit 9e50b2a6d3570c4f86fcfca6a5ce59947c0105ec\nAuthor: retoor \nDate: Thu Mar 27 21:14:01 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex aef0cdd..8b1f8ad 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -35,6 +35,6 @@ class ChatService(BaseService):\n uid=channel_message['uid'],\n user_nick=user['nick']\n ))\n- async self.app.create_task(self.services.notification.create_channel_message(channel_message_uid))\n+ await self.app.create_task(self.services.notification.create_channel_message(channel_message_uid))\n \n return True"} +{"repo": ".", "date": "2025-03-28", "line": "feat: Initialized Ubuntu terminal environment with Dockerfile and basic setup", "commit": "5fbcadad8bad7b8dd7ac3279938ff50db6b5d380", "diff": "commit 5fbcadad8bad7b8dd7ac3279938ff50db6b5d380\nAuthor: retoor \nDate: Fri Mar 28 02:41:57 2025 +0100\n\n Terminal Update.\n\ndiff --git a/DockerfileUbuntu b/DockerfileUbuntu\nnew file mode 100644\nindex 0000000..45c6038\n--- /dev/null\n+++ b/DockerfileUbuntu\n@@ -0,0 +1,11 @@\n+FROM ubuntu:latest\n+\n+RUN apt update && apt install libreadline-dev libcurl4-openssl-dev libssl-dev libncurses5-dev libncursesw5-dev libsqlite3-dev libreadline6-dev zlib1g-dev libbz2-dev libffi-dev liblzma-dev python3 python3-pip python3-venv libjson-c-dev vim htop git curl wget -y\n+\n+\n+RUN chmod +x r\n+\n+RUN cp r /usr/local/bin\n+\n+CMD [\"r\"]\ndiff --git a/Makefile b/Makefile\nindex c9a7a28..878e699 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -11,12 +11,15 @@ python:\n dump:\n \t@$(PYTHON) -m snek.dump\n \n+build:\n+\n+\n run:\n \t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\n \t\n install:\n \tpython3.12 -m venv .venv \n \t$(PIP) install -e .\n-\n+\tdocker build -f DockerfileUbuntu -t snek_ubuntu .\n \n \ndiff --git a/src/snek/system/terminal.py b/src/snek/system/terminal.py\nindex 6a91040..80dda17 100644\n--- a/src/snek/system/terminal.py\n+++ b/src/snek/system/terminal.py\n@@ -32,7 +32,7 @@ class TerminalSession:\n \n async def read_output(self, ws):\n self.sockets.append(ws)\n- if len(self.sockets) > 1 and self.buffer:\n+ if len(self.sockets) > 1 and self.history:\n start = 0\n try:\n start = self.history.index(b'\\n')\ndiff --git a/src/snek/view/terminal.py b/src/snek/view/terminal.py\nindex fac680c..6596f2c 100644\n--- a/src/snek/view/terminal.py\n+++ b/src/snek/view/terminal.py\n@@ -30,7 +30,10 @@ class TerminalSocketView(BaseView):\n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n root = await self.prepare_drive()\n \n- command = f\"docker run -v ./{root}/:/root --rm -it --memory 512M --cpus=0.5 -w /root ubuntu:latest /bin/bash\"\n+\n+ \n+\n+ command = f\"docker run -v ./{root}/:/root -it --memory 512M --cpus=0.5 -w /root snek_ubuntu /bin/bash\"\n \n session = self.user_sessions.get(user[\"uid\"])\n if not session:\ndiff --git a/terminal/.bashrc b/terminal/.bashrc\nindex e4022a9..ee7d93f 100644\n--- a/terminal/.bashrc\n+++ b/terminal/.bashrc\n@@ -92,11 +92,6 @@ if [ -f ~/.bash_aliases ]; then\n fi\n \n \n-cp ~/r /usr/local/bin \n-\n-chmod +x /usr/local/bin/r\n-\n-apt update && apt install libreadline-dev libcurl4-openssl-dev libssl-dev libncurses5-dev libncursesw5-dev libsqlite3-dev libreadline6-dev zlib1g-dev libbz2-dev libffi-dev liblzma-dev python3 python3-pip python3-venv libjson-c-dev vim htop git -y\n \n echo \"R is installed. Type r to run it.\"\n \ndiff --git a/terminal/r b/terminal/r\ndeleted file mode 100755\nindex 2cc65df..0000000\nBinary files a/terminal/r and /dev/null differ"} +{"repo": ".", "date": "2025-03-29", "line": "fix: Adjusted avatar size in message template", "commit": "fe1b3d6d191176111538f1ba06018e14c44ca8f9", "diff": "commit fe1b3d6d191176111538f1ba06018e14c44ca8f9\nAuthor: retoor \nDate: Sat Mar 29 01:21:33 2025 +0100\n\n Fixed avatar issue\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex 0b475d1..31a6e74 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -254,6 +254,7 @@ input[type=\"text\"], .chat-input textarea {\n }\n \n .avatar {\n+ \n opacity: 0;\n }\n \ndiff --git a/src/snek/templates/message.html b/src/snek/templates/message.html\nindex e8afc31..9773ae1 100644\n--- a/src/snek/templates/message.html\n+++ b/src/snek/templates/message.html\n@@ -1 +1 @@\n-
{{user_nick}}
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}
\n+
{{user_nick}}
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}
"} +{"repo": ".", "date": "2025-03-29", "line": "feat: Added webdav support for file management", "commit": "29139d5d0c18ad2b0ebd32db9a0b629c45c0a651", "diff": "commit 29139d5d0c18ad2b0ebd32db9a0b629c45c0a651\nAuthor: retoor \nDate: Sat Mar 29 07:13:23 2025 +0100\n\n Added webdav.\n\ndiff --git a/pyproject.toml b/pyproject.toml\nindex e1075c9..6fbf200 100644\n--- a/pyproject.toml\n+++ b/pyproject.toml\n@@ -15,6 +15,8 @@ keywords = [\"chat\", \"snek\", \"molodetz\"]\n requires-python = \">=3.12\"\n dependencies = [\n \"mkdocs>=1.4.0\",\n+ \"lxml\",\n+\n \"shed\",\n \"beautifulsoup4\",\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex dd3e0f3..984fcf3 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -1,13 +1,14 @@\n-import pathlib\n import asyncio\n-\n import logging\n+import pathlib\n import time\n \n-from snek.view.threads import ThreadsView \n+from snek.view.threads import ThreadsView\n \n logging.basicConfig(level=logging.DEBUG)\n \n+from concurrent.futures import ThreadPoolExecutor\n+\n from aiohttp import web\n from aiohttp_session import (\n get_session as session_get,\n@@ -24,23 +25,24 @@ from snek.system import http\n from snek.system.cache import Cache\n from snek.system.markdown import MarkdownExtension\n from snek.system.middleware import cors_middleware\n-from snek.system.template import LinkifyExtension, PythonExtension,EmojiExtension\n+from snek.system.profiler import profiler_handler\n+from snek.system.template import EmojiExtension, LinkifyExtension, PythonExtension\n from snek.view.about import AboutHTMLView, AboutMDView\n+from snek.view.avatar import AvatarView\n from snek.view.docs import DocsHTMLView, DocsMDView\n+from snek.view.drive import DriveView\n from snek.view.index import IndexView\n from snek.view.login import LoginView\n from snek.view.logout import LogoutView\n from snek.view.register import RegisterView\n from snek.view.rpc import RPCView\n+from snek.view.search_user import SearchUserView\n from snek.view.status import StatusView\n-from snek.view.web import WebView\n+from snek.view.terminal import TerminalSocketView, TerminalView\n from snek.view.upload import UploadView\n-from snek.view.search_user import SearchUserView \n-from snek.view.avatar import AvatarView\n-from snek.system.profiler import profiler_handler\n-from snek.view.terminal import TerminalView, TerminalSocketView\n-from snek.view.drive import DriveView\n-from concurrent.futures import ThreadPoolExecutor\n+from snek.view.web import WebView\n+from snek.webdav import WebdavApplication\n+\n SESSION_KEY = b\"c79a0c5fda4b424189c427d28c9f7c34\"\n \n \n@@ -50,6 +52,15 @@ async def session_middleware(request, handler):\n response = await handler(request)\n return response\n \n+\n+@web.middleware\n+async def trailing_slash_middleware(request, handler):\n+ if request.path and not request.path.endswith(\"/\"):\n+ raise web.HTTPFound(request.path + \"/\")\n+ return await handler(request)\n+\n+\n class Application(BaseApplication):\n \n def __init__(self, *args, **kwargs):\n@@ -68,9 +79,9 @@ class Application(BaseApplication):\n self.jinja2_env.add_extension(LinkifyExtension)\n self.jinja2_env.add_extension(PythonExtension)\n self.jinja2_env.add_extension(EmojiExtension)\n- \n+\n self.setup_router()\n- \n+\n self.cache = Cache(self)\n self.services = get_services(app=self)\n self.mappers = get_mappers(app=self)\n@@ -81,7 +92,7 @@ class Application(BaseApplication):\n \n async def task_runner(self):\n while True:\n- task = await self.tasks.get() \n+ task = await self.tasks.get()\n self.db.begin()\n try:\n task_start = time.time()\n@@ -93,20 +104,20 @@ class Application(BaseApplication):\n print(ex)\n self.db.commit()\n \n- async def prepare_database(self,app):\n+ async def prepare_database(self, app):\n self.db.query(\"PRAGMA journal_mode=WAL\")\n self.db.query(\"PRAGMA syncnorm=off\")\n \n try:\n if not self.db[\"user\"].has_index(\"username\"):\n self.db[\"user\"].create_index(\"username\", unique=True)\n- if not self.db[\"channel_member\"].has_index([\"channel_uid\",\"user_uid\"]):\n- self.db[\"channel_member\"].create_index([\"channel_uid\",\"user_uid\"])\n- if not self.db[\"channel_message\"].has_index([\"channel_uid\",\"user_uid\"]):\n- self.db[\"channel_message\"].create_index([\"channel_uid\",\"user_uid\"])\n+ if not self.db[\"channel_member\"].has_index([\"channel_uid\", \"user_uid\"]):\n+ self.db[\"channel_member\"].create_index([\"channel_uid\", \"user_uid\"])\n+ if not self.db[\"channel_message\"].has_index([\"channel_uid\", \"user_uid\"]):\n+ self.db[\"channel_message\"].create_index([\"channel_uid\", \"user_uid\"])\n except:\n- pass \n- \n+ pass\n+\n await app.services.drive.prepare_all()\n self.loop.create_task(self.task_runner())\n \n@@ -145,6 +156,8 @@ class Application(BaseApplication):\n self.router.add_view(\"/terminal.html\", TerminalView)\n self.router.add_view(\"/drive.json\", DriveView)\n self.router.add_view(\"/drive/{drive}.json\", DriveView)\n+ self.webdav = WebdavApplication(self)\n+ self.add_subapp(\"/webdav\", self.webdav)\n \n self.add_subapp(\n \"/docs\",\n@@ -174,17 +187,21 @@ class Application(BaseApplication):\n channels = []\n if not context:\n context = {}\n- if request.session.get(\"uid\"): \n- async for subscribed_channel in self.services.channel_member.find(user_uid=request.session.get(\"uid\"), deleted_at=None, is_banned=False):\n+ if request.session.get(\"uid\"):\n+ async for subscribed_channel in self.services.channel_member.find(\n+ user_uid=request.session.get(\"uid\"), deleted_at=None, is_banned=False\n+ ):\n item = {}\n- other_user = await self.services.channel_member.get_other_dm_user(subscribed_channel[\"channel_uid\"], request.session.get(\"uid\"))\n+ other_user = await self.services.channel_member.get_other_dm_user(\n+ subscribed_channel[\"channel_uid\"], request.session.get(\"uid\")\n+ )\n parent_object = await subscribed_channel.get_channel()\n- last_message =await parent_object.get_last_message()\n- color = None \n+ last_message = await parent_object.get_last_message()\n+ color = None\n if last_message:\n last_message_user = await last_message.get_user()\n color = last_message_user[\"color\"]\n- item['color'] = color\n+ item[\"color\"] = color\n item[\"last_message_on\"] = parent_object[\"last_message_on\"]\n item[\"is_private\"] = parent_object[\"tag\"] == \"dm\"\n if other_user:\n@@ -193,19 +210,22 @@ class Application(BaseApplication):\n else:\n item[\"name\"] = subscribed_channel[\"label\"]\n item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n- item['new_count'] = subscribed_channel['new_count'] \n- \n+ item[\"new_count\"] = subscribed_channel[\"new_count\"]\n+\n print(item)\n channels.append(item)\n- \n- channels.sort(key=lambda x: x['last_message_on'] or '', reverse=True)\n- if not 'channels' in context:\n- context['channels'] = channels\n- if not 'user' in context:\n- context['user'] = await self.services.user.get(uid=request.session.get(\"uid\"))\n+\n+ channels.sort(key=lambda x: x[\"last_message_on\"] or \"\", reverse=True)\n+ if \"channels\" not in context:\n+ context[\"channels\"] = channels\n+ if \"user\" not in context:\n+ context[\"user\"] = await self.services.user.get(\n+ uid=request.session.get(\"uid\")\n+ )\n \n return await super().render_template(template, request, context)\n \n+\n executor = ThreadPoolExecutor(max_workers=200)\n \n loop = asyncio.get_event_loop()\n@@ -213,8 +233,10 @@ loop.set_default_executor(executor)\n \n \n+\n async def main():\n await web._run_app(app, port=8081, host=\"0.0.0.0\")\n \n+\n if __name__ == \"__main__\":\n asyncio.run(main())\ndiff --git a/src/snek/dump.py b/src/snek/dump.py\nindex 1b7eb6b..b254756 100644\n--- a/src/snek/dump.py\n+++ b/src/snek/dump.py\n@@ -1,31 +1,39 @@\n import asyncio\n-import json \n \n from snek.app import app\n \n+\n async def fix_message(message):\n- message = dict(\n- uid=message['uid'],\n- user_uid=message['user_uid'],\n- text=message['message'],\n- sent=message['created_at']\n- )\n- user = await app.services.user.get(uid=message['user_uid'])\n- message['user'] = user and user['username'] or None\n- return (message['user'] or '') + ': ' + (message['text'] or '')\n+ message = {\n+ \"uid\": message[\"uid\"],\n+ \"user_uid\": message[\"user_uid\"],\n+ \"text\": message[\"message\"],\n+ \"sent\": message[\"created_at\"],\n+ }\n+ user = await app.services.user.get(uid=message[\"user_uid\"])\n+ message[\"user\"] = user and user[\"username\"] or None\n+ return (message[\"user\"] or \"\") + \": \" + (message[\"text\"] or \"\")\n+\n \n async def dump_public_channels():\n result = []\n- for channel in app.db['channel'].find(is_private=False,is_listed=True,tag='public'):\n+ for channel in app.db[\"channel\"].find(\n+ is_private=False, is_listed=True, tag=\"public\"\n+ ):\n print(f\"Dumping channel: {channel['label']}.\")\n- result += [await fix_message(record) for record in app.db['channel_message'].find(channel_uid=channel['uid'],order_by='created_at')]\n+ result += [\n+ await fix_message(record)\n+ for record in app.db[\"channel_message\"].find(\n+ channel_uid=channel[\"uid\"], order_by=\"created_at\"\n+ )\n+ ]\n print(\"Dump succesfull!\")\n print(\"Converting to json.\")\n print(\"Converting succesful, now writing to dump.json\")\n- with open(\"dump.txt\",\"w\") as f:\n- f.write('\\n\\n'.join(result))\n+ with open(\"dump.txt\", \"w\") as f:\n+ f.write(\"\\n\\n\".join(result))\n print(\"Dump written to dump.json\")\n- \n \n-if __name__ == '__main__':\n+\n+if __name__ == \"__main__\":\n asyncio.run(dump_public_channels())\ndiff --git a/src/snek/form/search_user.py b/src/snek/form/search_user.py\nindex 6f431f0..7e946b9 100644\n--- a/src/snek/form/search_user.py\n+++ b/src/snek/form/search_user.py\n@@ -16,4 +16,3 @@ class SearchUserForm(Form):\n action = FormButtonElement(\n name=\"action\", value=\"submit\", text=\"Search\", type=\"button\"\n )\n-\ndiff --git a/src/snek/mapper/__init__.py b/src/snek/mapper/__init__.py\nindex e4c67b0..96053ea 100644\n--- a/src/snek/mapper/__init__.py\n+++ b/src/snek/mapper/__init__.py\n@@ -3,10 +3,10 @@ import functools\n from snek.mapper.channel import ChannelMapper\n from snek.mapper.channel_member import ChannelMemberMapper\n from snek.mapper.channel_message import ChannelMessageMapper\n+from snek.mapper.drive import DriveMapper\n+from snek.mapper.drive_item import DriveItemMapper\n from snek.mapper.notification import NotificationMapper\n from snek.mapper.user import UserMapper\n-from snek.mapper.drive import DriveMapper \n-from snek.mapper.drive_item import DriveItemMapper\n from snek.system.object import Object\n \n \ndiff --git a/src/snek/mapper/drive.py b/src/snek/mapper/drive.py\nindex 970788a..c92c687 100644\n--- a/src/snek/mapper/drive.py\n+++ b/src/snek/mapper/drive.py\n@@ -3,5 +3,5 @@ from snek.system.mapper import BaseMapper\n \n \n class DriveMapper(BaseMapper):\n- table_name = 'drive'\n- model_class = DriveModel \n+ table_name = \"drive\"\n+ model_class = DriveModel\ndiff --git a/src/snek/mapper/drive_item.py b/src/snek/mapper/drive_item.py\nindex c35afe1..3d17a61 100644\n--- a/src/snek/mapper/drive_item.py\n+++ b/src/snek/mapper/drive_item.py\n@@ -1,7 +1,8 @@\n+from snek.model.drive_item import DriveItemModel\n from snek.system.mapper import BaseMapper\n-from snek.model.drive_item import DriveItemModel \n+\n \n class DriveItemMapper(BaseMapper):\n- \n+\n model_class = DriveItemModel\n- table_name = 'drive_item'\n+ table_name = \"drive_item\"\ndiff --git a/src/snek/model/channel.py b/src/snek/model/channel.py\nindex 649299e..183ddb0 100644\n--- a/src/snek/model/channel.py\n+++ b/src/snek/model/channel.py\n@@ -12,11 +12,16 @@ class ChannelModel(BaseModel):\n index = ModelField(name=\"index\", required=True, kind=int, value=1000)\n last_message_on = ModelField(name=\"last_message_on\", required=False, kind=str)\n \n- async def get_last_message(self)->ChannelMessageModel:\n- 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'])):\n- \n- return await self.app.services.channel_message.get(uid=model['uid'])\n+ async def get_last_message(self) -> ChannelMessageModel:\n+ async for model in self.app.services.channel_message.query(\n+ \"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1\",\n+ {\"channel_uid\": self[\"uid\"]},\n+ ):\n+\n+ return await self.app.services.channel_message.get(uid=model[\"uid\"])\n return None\n \n async def get_members(self):\n- return await self.app.services.channel_member.find(channel_uid=self['uid'],deleted_at=None,is_banned=False)\n+ return await self.app.services.channel_member.find(\n+ channel_uid=self[\"uid\"], deleted_at=None, is_banned=False\n+ )\ndiff --git a/src/snek/model/channel_member.py b/src/snek/model/channel_member.py\nindex 9689fa5..54b0418 100644\n--- a/src/snek/model/channel_member.py\n+++ b/src/snek/model/channel_member.py\n@@ -16,25 +16,26 @@ class ChannelMemberModel(BaseModel):\n new_count = ModelField(name=\"new_count\", required=False, kind=int, value=0)\n \n async def get_user(self):\n- return await self.app.services.user.get(uid=self['user_uid'])\n- \n+ return await self.app.services.user.get(uid=self[\"user_uid\"])\n+\n async def get_channel(self):\n- return await self.app.services.channel.get(uid=self['channel_uid'])\n+ return await self.app.services.channel.get(uid=self[\"channel_uid\"])\n \n async def get_name(self):\n channel = await self.get_channel()\n if channel[\"tag\"] == \"dm\":\n user = await self.get_other_dm_user()\n- return user['nick']\n- return channel['name'] or self['label']\n+ return user[\"nick\"]\n+ return channel[\"name\"] or self[\"label\"]\n \n async def get_other_dm_user(self):\n channel = await self.get_channel()\n if channel[\"tag\"] != \"dm\":\n return None\n- \n- async for model in self.app.services.channel_member.find(channel_uid=channel['uid']):\n- if model[\"uid\"] != self['uid']:\n+\n+ async for model in self.app.services.channel_member.find(\n+ channel_uid=channel[\"uid\"]\n+ ):\n+ if model[\"uid\"] != self[\"uid\"]:\n return await self.app.services.user.get(uid=model[\"user_uid\"])\n return await self.get_user()\n- \ndiff --git a/src/snek/model/channel_message.py b/src/snek/model/channel_message.py\nindex 4b84fdc..524a8a4 100644\n--- a/src/snek/model/channel_message.py\n+++ b/src/snek/model/channel_message.py\n@@ -8,8 +8,8 @@ class ChannelMessageModel(BaseModel):\n message = ModelField(name=\"message\", required=True, kind=str)\n html = ModelField(name=\"html\", required=False, kind=str)\n \n- async def get_user(self)->UserModel:\n+ async def get_user(self) -> UserModel:\n return await self.app.services.user.get(uid=self[\"user_uid\"])\n- \n+\n async def get_channel(self):\n- return await self.app.services.channel.get(uid=self[\"channel_uid\"])\n\\ No newline at end of file\n+ return await self.app.services.channel.get(uid=self[\"channel_uid\"])\ndiff --git a/src/snek/model/drive.py b/src/snek/model/drive.py\nindex 3936d97..df17d0f 100644\n--- a/src/snek/model/drive.py\n+++ b/src/snek/model/drive.py\n@@ -1,13 +1,14 @@\n-from snek.system.model import BaseModel,ModelField\n+from snek.system.model import BaseModel, ModelField\n \n \n class DriveModel(BaseModel):\n \n user_uid = ModelField(name=\"user_uid\", required=True)\n- name = ModelField(name='name', required=False, type=str) \n+ name = ModelField(name=\"name\", required=False, type=str)\n \n @property\n async def items(self):\n- async for drive_item in self.app.services.drive_item.find(drive_uid=self['uid']):\n+ async for drive_item in self.app.services.drive_item.find(\n+ drive_uid=self[\"uid\"]\n+ ):\n yield drive_item\n-\ndiff --git a/src/snek/model/drive_item.py b/src/snek/model/drive_item.py\nindex 728cd89..6e28f84 100644\n--- a/src/snek/model/drive_item.py\n+++ b/src/snek/model/drive_item.py\n@@ -1,18 +1,20 @@\n-from snek.system.model import BaseModel,ModelField \n import mimetypes\n \n+from snek.system.model import BaseModel, ModelField\n+\n+\n class DriveItemModel(BaseModel):\n- drive_uid = ModelField(name=\"drive_uid\", required=True,kind=str)\n- name = ModelField(name=\"name\", required=True,kind=str)\n- path = ModelField(name=\"path\", required=True,kind=str)\n- file_type = ModelField(name=\"file_type\", required=True,kind=str) \n- file_size = ModelField(name=\"file_size\", required=True,kind=int)\n- \n+ drive_uid = ModelField(name=\"drive_uid\", required=True, kind=str)\n+ name = ModelField(name=\"name\", required=True, kind=str)\n+ path = ModelField(name=\"path\", required=True, kind=str)\n+ file_type = ModelField(name=\"file_type\", required=True, kind=str)\n+ file_size = ModelField(name=\"file_size\", required=True, kind=int)\n+\n @property\n def extension(self):\n- return self['name'].split('.')[-1]\n- \n- @property \n+ return self[\"name\"].split(\".\")[-1]\n+\n+ @property\n def mime_type(self):\n- mimetype,_ = mimetypes.guess_type(self['name'])\n- return mimetype \n+ mimetype, _ = mimetypes.guess_type(self[\"name\"])\n+ return mimetype\ndiff --git a/src/snek/model/user.py b/src/snek/model/user.py\nindex 0621ecf..a35d890 100644\n--- a/src/snek/model/user.py\n+++ b/src/snek/model/user.py\n@@ -18,10 +18,7 @@ class UserModel(BaseModel):\n regex=r\"^[a-zA-Z0-9_-+/]+$\",\n )\n color = ModelField(\n- name =\"color\",\n- required=True,\n- kind=str\n )\n email = ModelField(\n name=\"email\",\n@@ -33,5 +30,7 @@ class UserModel(BaseModel):\n last_ping = ModelField(name=\"last_ping\", required=False, kind=str)\n \n async def get_channel_members(self):\n- async for channel_member in self.app.services.channel_member.find(user_uid=self['uid'],is_banned=False,deleted_at=None):\n+ async for channel_member in self.app.services.channel_member.find(\n+ user_uid=self[\"uid\"], is_banned=False, deleted_at=None\n+ ):\n yield channel_member\ndiff --git a/src/snek/service/__init__.py b/src/snek/service/__init__.py\nindex e521c7b..4059f77 100644\n--- a/src/snek/service/__init__.py\n+++ b/src/snek/service/__init__.py\n@@ -4,12 +4,12 @@ from snek.service.channel import ChannelService\n from snek.service.channel_member import ChannelMemberService\n from snek.service.channel_message import ChannelMessageService\n from snek.service.chat import ChatService\n+from snek.service.drive import DriveService\n+from snek.service.drive_item import DriveItemService\n from snek.service.notification import NotificationService\n from snek.service.socket import SocketService\n from snek.service.user import UserService\n from snek.service.util import UtilService\n-from snek.service.drive import DriveService\n-from snek.service.drive_item import DriveItemService\n from snek.system.object import Object\n \n \n@@ -26,7 +26,7 @@ def get_services(app):\n \"notification\": NotificationService(app=app),\n \"util\": UtilService(app=app),\n \"drive\": DriveService(app=app),\n- \"drive_item\": DriveItemService(app=app)\n+ \"drive_item\": DriveItemService(app=app),\n }\n )\n \ndiff --git a/src/snek/service/channel.py b/src/snek/service/channel.py\nindex 95ceef7..b90e66f 100644\n--- a/src/snek/service/channel.py\n+++ b/src/snek/service/channel.py\n@@ -1,30 +1,28 @@\n+from datetime import datetime\n+\n+from snek.system.model import now\n from snek.system.service import BaseService\n-from datetime import datetime \n \n-from snek.system.model import now \n \n class ChannelService(BaseService):\n mapper_name = \"channel\"\n \n- async def get(\n- self,\n- uid=None,\n- **kwargs):\n+ async def get(self, uid=None, **kwargs):\n if uid:\n- kwargs['uid'] = uid \n+ kwargs[\"uid\"] = uid\n result = await super().get(**kwargs)\n if result:\n- return result \n- del kwargs['uid']\n- kwargs['name'] = uid \n+ return result\n+ del kwargs[\"uid\"]\n+ kwargs[\"name\"] = uid\n result = await super().get(**kwargs)\n if result:\n return result\n result = await super().get(**kwargs)\n if result:\n return result\n- return None \n+ return None\n return await super().get(**kwargs)\n \n async def create(\n@@ -53,38 +51,34 @@ class ChannelService(BaseService):\n raise Exception(f\"Failed to create channel: {model.errors}.\")\n \n async def get_dm(self, user1, user2):\n- channel_member = await self.services.channel_member.get_dm(\n- user1, user2 \n- )\n+ channel_member = await self.services.channel_member.get_dm(user1, user2)\n if channel_member:\n return await self.get(uid=channel_member[\"channel_uid\"])\n- channel = await self.create(\n- \"DM\", user1, tag=\"dm\" \n- )\n- await self.services.channel_member.create_dm(\n- channel[\"uid\"], user1, user2 \n- )\n- return channel \n+ channel = await self.create(\"DM\", user1, tag=\"dm\")\n+ await self.services.channel_member.create_dm(channel[\"uid\"], user1, user2)\n+ return channel\n \n async def get_users(self, channel_uid):\n- users = []\n async for channel_member in self.services.channel_member.find(\n channel_uid=channel_uid,\n is_banned=False,\n is_muted=False,\n deleted_at=None,\n ):\n- user = await self.services.user.get(uid=channel_member[\"user_uid\"])\n+ user = await self.services.user.get(uid=channel_member[\"user_uid\"])\n if user:\n yield user\n+\n async def get_online_users(self, channel_uid):\n- users = []\n async for user in self.get_users(channel_uid):\n if not user[\"last_ping\"]:\n continue\n \n- if (datetime.fromisoformat(now()) - datetime.fromisoformat(user[\"last_ping\"])).total_seconds() < 20:\n- yield user \n+ if (\n+ datetime.fromisoformat(now())\n+ - datetime.fromisoformat(user[\"last_ping\"])\n+ ).total_seconds() < 20:\n+ yield user\n \n async def get_for_user(self, user_uid):\n async for channel_member in self.services.channel_member.find(\n@@ -93,7 +87,7 @@ class ChannelService(BaseService):\n deleted_at=None,\n ):\n channel = await self.get(uid=channel_member[\"channel_uid\"])\n- yield channel \n+ yield channel\n \n async def ensure_public_channel(self, created_by_uid):\n model = await self.get(is_listed=True, tag=\"public\")\ndiff --git a/src/snek/service/channel_member.py b/src/snek/service/channel_member.py\nindex 5c6c7ee..16d1887 100644\n--- a/src/snek/service/channel_member.py\n+++ b/src/snek/service/channel_member.py\n@@ -6,10 +6,7 @@ class ChannelMemberService(BaseService):\n mapper_name = \"channel_member\"\n \n async def mark_as_read(self, channel_uid, user_uid):\n- channel_member = await self.get(\n- channel_uid=channel_uid,\n- user_uid=user_uid\n- )\n+ channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)\n channel_member[\"new_count\"] = 0\n return await self.save(channel_member)\n \n@@ -24,7 +21,7 @@ class ChannelMemberService(BaseService):\n ):\n model = await self.get(channel_uid=channel_uid, user_uid=user_uid)\n if model:\n- if model['is_banned']:\n+ if model[\"is_banned\"]:\n return False\n return model\n model = await self.new()\n@@ -39,30 +36,32 @@ class ChannelMemberService(BaseService):\n if await self.save(model):\n return model\n raise Exception(f\"Failed to create channel member: {model.errors}.\")\n- \n- async def get_dm(self,from_user, to_user):\n- 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)):\n+\n+ async def get_dm(self, from_user, to_user):\n+ async for model in self.query(\n+ \"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 \",\n+ {\"from_user\": from_user, \"to_user\": to_user},\n+ ):\n return model\n if not from_user == to_user:\n- return None \n- 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)):\n- \n- return model \n- \n+ return None\n+ async for model in self.query(\n+ \"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 \",\n+ {\"from_user\": from_user, \"to_user\": to_user},\n+ ):\n+\n+ return model\n+\n async def get_other_dm_user(self, channel_uid, user_uid):\n channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)\n- channel = await self.services.channel.get(uid=channel_member['channel_uid'])\n+ channel = await self.services.channel.get(uid=channel_member[\"channel_uid\"])\n if channel[\"tag\"] != \"dm\":\n return None\n async for model in self.services.channel_member.find(channel_uid=channel_uid):\n- if model[\"uid\"] != channel_member['uid']:\n+ if model[\"uid\"] != channel_member[\"uid\"]:\n return await self.services.user.get(uid=model[\"user_uid\"])\n \n- async def create_dm(self,channel_uid, from_user_uid, to_user_uid):\n+ async def create_dm(self, channel_uid, from_user_uid, to_user_uid):\n result = await self.create(channel_uid, from_user_uid)\n await self.create(channel_uid, to_user_uid)\n- return result \n-\n-\n-\n-\n+ return result\ndiff --git a/src/snek/service/channel_message.py b/src/snek/service/channel_message.py\nindex 9683631..f8a000f 100644\n--- a/src/snek/service/channel_message.py\n+++ b/src/snek/service/channel_message.py\n@@ -1,71 +1,93 @@\n from snek.system.service import BaseService\n-import jinja2 \n+\n \n class ChannelMessageService(BaseService):\n mapper_name = \"channel_message\"\n \n async def create(self, channel_uid, user_uid, message):\n model = await self.new()\n- \n+\n model[\"channel_uid\"] = channel_uid\n model[\"user_uid\"] = user_uid\n model[\"message\"] = message\n- \n- context = {\n- \n- }\n- \n- \n- record = model.record \n+\n+ context = {}\n+\n+ record = model.record\n context.update(record)\n user = await self.app.services.user.get(uid=user_uid)\n- context.update(dict(\n- user_uid=user['uid'],\n- username=user['username'],\n- user_nick=user['nick'],\n- color=user['color']\n- ))\n+ context.update(\n+ {\n+ \"user_uid\": user[\"uid\"],\n+ \"username\": user[\"username\"],\n+ \"user_nick\": user[\"nick\"],\n+ \"color\": user[\"color\"],\n+ }\n+ )\n try:\n template = self.app.jinja2_env.get_template(\"message.html\")\n model[\"html\"] = template.render(**context)\n except Exception as ex:\n- print(ex,flush=True)\n- \n+ print(ex, flush=True)\n+\n if await self.save(model):\n return model\n raise Exception(f\"Failed to create channel message: {model.errors}.\")\n- \n+\n async def to_extended_dict(self, message):\n user = await self.services.user.get(uid=message[\"user_uid\"])\n if not user:\n return {}\n return {\n \"uid\": message[\"uid\"],\n- \"color\": user['color'],\n+ \"color\": user[\"color\"],\n \"user_uid\": message[\"user_uid\"],\n \"channel_uid\": message[\"channel_uid\"],\n- \"user_nick\": user['nick'],\n+ \"user_nick\": user[\"nick\"],\n \"message\": message[\"message\"],\n \"created_at\": message[\"created_at\"],\n- \"html\": message['html'],\n- \"username\": user['username'] \n+ \"html\": message[\"html\"],\n+ \"username\": user[\"username\"],\n }\n \n- async def offset(self, channel_uid, page=0, timestamp = None, page_size=30):\n+ async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):\n results = []\n- offset = page * page_size \n+ offset = page * page_size\n try:\n if timestamp:\n- 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)):\n+ async for model in self.query(\n+ \"SELECT * FROM channel_message WHERE channel_uid=:channel_uid AND created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size OFFSET :offset\",\n+ {\n+ \"channel_uid\": channel_uid,\n+ \"page_size\": page_size,\n+ \"offset\": offset,\n+ \"timestamp\": timestamp,\n+ },\n+ ):\n results.append(model)\n elif page > 0:\n- 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 )):\n+ async for model in self.query(\n+ \"SELECT * FROM channel_message WHERE channel_uid=:channel_uid WHERE created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size\",\n+ {\n+ \"channel_uid\": channel_uid,\n+ \"page_size\": page_size,\n+ \"offset\": offset,\n+ \"timestamp\": timestamp,\n+ },\n+ ):\n results.append(model)\n- else: \n- 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)):\n+ else:\n+ async for model in self.query(\n+ \"SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT :page_size OFFSET :offset\",\n+ {\n+ \"channel_uid\": channel_uid,\n+ \"page_size\": page_size,\n+ \"offset\": offset,\n+ },\n+ ):\n results.append(model)\n \n- except: \n+ except:\n pass\n- results.sort(key=lambda x: x['created_at'])\n- return results \n+ results.sort(key=lambda x: x[\"created_at\"])\n+ return results\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex 8b1f8ad..388d5c0 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -1,40 +1,39 @@\n-\n-\n-\n from snek.system.model import now\n from snek.system.service import BaseService\n \n \n class ChatService(BaseService):\n \n- async def send(self,user_uid, channel_uid, message):\n+ async def send(self, user_uid, channel_uid, message):\n channel = await self.services.channel.get(uid=channel_uid)\n if not channel:\n raise Exception(\"Channel not found.\")\n channel_message = await self.services.channel_message.create(\n- channel_uid, \n- user_uid, \n- message\n+ channel_uid, user_uid, message\n )\n channel_message_uid = channel_message[\"uid\"]\n- \n- \n+\n user = await self.services.user.get(uid=user_uid)\n- channel['last_message_on'] = now()\n+ channel[\"last_message_on\"] = now()\n await self.services.channel.save(channel)\n- \n- await self.services.socket.broadcast(channel_uid, dict(\n- message=channel_message[\"message\"],\n- html=channel_message[\"html\"],\n- user_uid=user_uid,\n- color=user['color'],\n- channel_uid=channel_uid,\n- created_at=channel_message[\"created_at\"], \n- updated_at=None,\n- username=user['username'],\n- uid=channel_message['uid'],\n- user_nick=user['nick']\n- ))\n- await self.app.create_task(self.services.notification.create_channel_message(channel_message_uid))\n- \n+\n+ await self.services.socket.broadcast(\n+ channel_uid,\n+ {\n+ \"message\": channel_message[\"message\"],\n+ \"html\": channel_message[\"html\"],\n+ \"user_uid\": user_uid,\n+ \"color\": user[\"color\"],\n+ \"channel_uid\": channel_uid,\n+ \"created_at\": channel_message[\"created_at\"],\n+ \"updated_at\": None,\n+ \"username\": user[\"username\"],\n+ \"uid\": channel_message[\"uid\"],\n+ \"user_nick\": user[\"nick\"],\n+ },\n+ )\n+ await self.app.create_task(\n+ self.services.notification.create_channel_message(channel_message_uid)\n+ )\n+\n return True\ndiff --git a/src/snek/service/drive.py b/src/snek/service/drive.py\nindex b90a959..38035c7 100644\n--- a/src/snek/service/drive.py\n+++ b/src/snek/service/drive.py\n@@ -2,14 +2,90 @@ from snek.system.service import BaseService\n \n \n class DriveService(BaseService):\n- \n+\n mapper_name = \"drive\"\n \n- EXTENSIONS_PICTURES = [\"jpg\",\"jpeg\",\"png\",\"gif\",\"svg\",\"webp\",\"tiff\"]\n- EXTENSIONS_VIDEOS = [\"mp4\",\"m4v\",\"mov\",\"wmv\",\"webm\",\"mkv\",\"mpg\",\"mpeg\",\"avi\",\"ogv\",\"ogg\",\"flv\",\"3gp\",\"3g2\"]\n- EXTENSIONS_ARCHIVES = [\"zip\",\"rar\",\"7z\",\"tar\",\"tar.gz\",\"tar.xz\",\"tar.bz2\",\"tar.lzma\",\"tar.lz\"]\n- EXTENSIONS_AUDIO = [\"mp3\",\"wav\",\"ogg\",\"flac\",\"m4a\",\"wma\",\"aac\",\"opus\",\"aiff\",\"au\",\"mid\",\"midi\"]\n- 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\"]\n+ EXTENSIONS_PICTURES = [\"jpg\", \"jpeg\", \"png\", \"gif\", \"svg\", \"webp\", \"tiff\"]\n+ EXTENSIONS_VIDEOS = [\n+ \"mp4\",\n+ \"m4v\",\n+ \"mov\",\n+ \"wmv\",\n+ \"webm\",\n+ \"mkv\",\n+ \"mpg\",\n+ \"mpeg\",\n+ \"avi\",\n+ \"ogv\",\n+ \"ogg\",\n+ \"flv\",\n+ \"3gp\",\n+ \"3g2\",\n+ ]\n+ EXTENSIONS_ARCHIVES = [\n+ \"zip\",\n+ \"rar\",\n+ \"7z\",\n+ \"tar\",\n+ \"tar.gz\",\n+ \"tar.xz\",\n+ \"tar.bz2\",\n+ \"tar.lzma\",\n+ \"tar.lz\",\n+ ]\n+ EXTENSIONS_AUDIO = [\n+ \"mp3\",\n+ \"wav\",\n+ \"ogg\",\n+ \"flac\",\n+ \"m4a\",\n+ \"wma\",\n+ \"aac\",\n+ \"opus\",\n+ \"aiff\",\n+ \"au\",\n+ \"mid\",\n+ \"midi\",\n+ ]\n+ EXTENSIONS_DOCS = [\n+ \"pdf\",\n+ \"doc\",\n+ \"docx\",\n+ \"xls\",\n+ \"xlsx\",\n+ \"ppt\",\n+ \"pptx\",\n+ \"txt\",\n+ \"md\",\n+ \"json\",\n+ \"csv\",\n+ \"xml\",\n+ \"html\",\n+ \"css\",\n+ \"js\",\n+ \"py\",\n+ \"sql\",\n+ \"rs\",\n+ \"toml\",\n+ \"yml\",\n+ \"yaml\",\n+ \"ini\",\n+ \"conf\",\n+ \"config\",\n+ \"log\",\n+ \"csv\",\n+ \"tsv\",\n+ \"java\",\n+ \"cs\",\n+ \"csproj\",\n+ \"scss\",\n+ \"less\",\n+ \"sass\",\n+ \"json\",\n+ \"lock\",\n+ \"lock.json\",\n+ \"jsonl\",\n+ ]\n \n async def get_drive_name_by_extension(self, extension):\n if extension.startswith(\".\"):\n@@ -26,54 +102,52 @@ class DriveService(BaseService):\n return \"Documents\"\n return \"My Drive\"\n \n- async def get_drive_by_extension(self,user_uid, extension):\n+ async def get_drive_by_extension(self, user_uid, extension):\n name = await self.get_drive_name_by_extension(extension)\n- return await self.get_or_create(user_uid=user_uid,name=name)\n+ return await self.get_or_create(user_uid=user_uid, name=name)\n \n- async def get_by_user(self, user_uid,name=None):\n- kwargs = dict(\n- user_uid = user_uid\n- )\n+ async def get_by_user(self, user_uid, name=None):\n+ kwargs = {\"user_uid\": user_uid}\n async for model in self.find(**kwargs):\n if not name:\n- yield model \n- elif model['name'] == name:\n yield model\n- elif not model['name'] and name == 'My Drive':\n- model['name'] = 'My Drive'\n+ elif model[\"name\"] == name:\n+ yield model\n+ elif not model[\"name\"] and name == \"My Drive\":\n+ model[\"name\"] = \"My Drive\"\n await self.save(model)\n- yield model \n+ yield model\n \n- async def get_or_create(self, user_uid,name=None,extensions=None):\n- kwargs = dict(user_uid=user_uid)\n+ async def get_or_create(self, user_uid, name=None, extensions=None):\n+ kwargs = {\"user_uid\": user_uid}\n if name:\n- kwargs['name'] = name\n+ kwargs[\"name\"] = name\n async for model in self.get_by_user(**kwargs):\n- return model \n+ return model\n \n model = await self.new()\n- model['user_uid'] = user_uid\n- model['name'] = name \n+ model[\"user_uid\"] = user_uid\n+ model[\"name\"] = name\n await self.save(model)\n- return model \n+ return model\n \n async def prepare_default_drives(self):\n async for drive_item in self.services.drive_item.find():\n extension = drive_item.extension\n- drive = await self.get_drive_by_extension(drive_item['user_uid'],extension)\n- if not drive_item['drive_uid'] == drive['uid']:\n- drive_item['drive_uid'] = drive['uid']\n+ drive = await self.get_drive_by_extension(drive_item[\"user_uid\"], extension)\n+ if not drive_item[\"drive_uid\"] == drive[\"uid\"]:\n+ drive_item[\"drive_uid\"] = drive[\"uid\"]\n await self.services.drive_item.save(drive_item)\n- \n+\n async def prepare_default_drives_for_user(self, user_uid):\n- await self.get_or_create(user_uid=user_uid,name=\"My Drive\")\n- await self.get_or_create(user_uid=user_uid,name=\"Shared Drive\")\n- await self.get_or_create(user_uid=user_uid,name=\"Pictures\")\n- await self.get_or_create(user_uid=user_uid,name=\"Videos\")\n- await self.get_or_create(user_uid=user_uid,name=\"Archives\")\n- await self.get_or_create(user_uid=user_uid,name=\"Documents\")\n+ await self.get_or_create(user_uid=user_uid, name=\"My Drive\")\n+ await self.get_or_create(user_uid=user_uid, name=\"Shared Drive\")\n+ await self.get_or_create(user_uid=user_uid, name=\"Pictures\")\n+ await self.get_or_create(user_uid=user_uid, name=\"Videos\")\n+ await self.get_or_create(user_uid=user_uid, name=\"Archives\")\n+ await self.get_or_create(user_uid=user_uid, name=\"Documents\")\n \n async def prepare_all(self):\n await self.prepare_default_drives()\n async for user in self.services.user.find():\n- await self.prepare_default_drives_for_user(user['uid']) \n+ await self.prepare_default_drives_for_user(user[\"uid\"])\ndiff --git a/src/snek/service/drive_item.py b/src/snek/service/drive_item.py\nindex 05a7da8..ce747c1 100644\n--- a/src/snek/service/drive_item.py\n+++ b/src/snek/service/drive_item.py\n@@ -1,19 +1,19 @@\n-from snek.system.service import BaseService \n+from snek.system.service import BaseService\n \n \n class DriveItemService(BaseService):\n \n mapper_name = \"drive_item\"\n \n- async def create(self, drive_uid, name, path, type_,size):\n- model = await self.new() \n- model['drive_uid'] = drive_uid \n- model['name'] = name \n- model['path'] = str(path) \n- model['extension'] = str(name).split(\".\")[-1]\n- model['file_type'] = type_ \n- model['file_size'] = size\n+ async def create(self, drive_uid, name, path, type_, size):\n+ model = await self.new()\n+ model[\"drive_uid\"] = drive_uid\n+ model[\"name\"] = name\n+ model[\"path\"] = str(path)\n+ model[\"extension\"] = str(name).split(\".\")[-1]\n+ model[\"file_type\"] = type_\n+ model[\"file_size\"] = size\n if await self.save(model):\n- return model \n+ return model\n errors = await model.errors\n raise Exception(f\"Failed to create drive item: {errors}.\")\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 1392044..a22e8ae 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -1,5 +1,6 @@\n-from snek.system.service import BaseService\n from snek.system.model import now\n+from snek.system.service import BaseService\n+\n \n class NotificationService(BaseService):\n mapper_name = \"notification\"\n@@ -7,13 +8,16 @@ class NotificationService(BaseService):\n async def mark_as_read(self, user_uid, channel_message_uid):\n model = await self.get(user_uid, object_uid=channel_message_uid)\n if not model:\n- return False \n- model['read_at'] = now()\n+ return False\n+ model[\"read_at\"] = now()\n await self.save(model)\n- return True \n- \n- async def get_unread_stats(self,user_uid):\n- 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))\n+ return True\n+\n+ async def get_unread_stats(self, user_uid):\n+ await self.query(\n+ \"SELECT object_type, COUNT(*) as count FROM notification WHERE user_uid=:user_uid AND read_at IS NULL GROUP BY object_type\",\n+ {\"user_uid\": user_uid},\n+ )\n \n async def create(self, object_uid, object_type, user_uid, message):\n model = await self.new()\n@@ -37,10 +41,10 @@ class NotificationService(BaseService):\n is_muted=False,\n deleted_at=None,\n ):\n- if not channel_member['new_count']:\n- channel_member['new_count'] = 0\n- channel_member['new_count'] += 1\n- \n+ if not channel_member[\"new_count\"]:\n+ channel_member[\"new_count\"] = 0\n+ channel_member[\"new_count\"] += 1\n+\n usr = await self.services.user.get(uid=channel_member[\"user_uid\"])\n if not usr:\n continue\n@@ -55,7 +59,7 @@ class NotificationService(BaseService):\n )\n try:\n await self.save(model)\n- except Exception as ex:\n+ except Exception:\n raise Exception(f\"Failed to create notification: {model.errors}.\")\n \n self.app.db.commit()\ndiff --git a/src/snek/service/socket.py b/src/snek/service/socket.py\nindex d941321..072a86f 100644\n--- a/src/snek/service/socket.py\n+++ b/src/snek/service/socket.py\n@@ -1,6 +1,4 @@\n from snek.model.user import UserModel\n-\n-\n from snek.system.service import BaseService\n \n \n@@ -9,28 +7,27 @@ class SocketService(BaseService):\n class Socket:\n def __init__(self, ws, user: UserModel):\n self.ws = ws\n- self.is_connected = True \n- self.user = user \n+ self.is_connected = True\n+ self.user = user\n \n async def send_json(self, data):\n if not self.is_connected:\n- return False \n+ return False\n try:\n await self.ws.send_json(data)\n except Exception as ex:\n- print(ex,flush=True)\n+ print(ex, flush=True)\n self.is_connected = False\n- return True \n+ return True\n \n async def close(self):\n if not self.is_connected:\n- return True \n- \n+ return True\n+\n await self.ws.close()\n self.is_connected = False\n- \n- return True \n \n+ return True\n \n def __init__(self, app):\n super().__init__(app)\n@@ -42,32 +39,31 @@ class SocketService(BaseService):\n s = self.Socket(ws, await self.app.services.user.get(uid=user_uid))\n self.sockets.add(s)\n if not self.users.get(user_uid):\n- self.users[user_uid] = set() \n+ self.users[user_uid] = set()\n self.users[user_uid].add(s)\n \n- async def subscribe(self, ws,channel_uid, user_uid):\n+ async def subscribe(self, ws, channel_uid, user_uid):\n return\n- if not channel_uid in self.subscriptions:\n+ if channel_uid not in self.subscriptions:\n self.subscriptions[channel_uid] = set()\n- s = self.Socket(ws,await self.app.services.user.get(uid=user_uid))\n+ s = self.Socket(ws, await self.app.services.user.get(uid=user_uid))\n self.subscriptions[channel_uid].add(s)\n \n async def send_to_user(self, user_uid, message):\n- count = 0 \n- for s in self.users.get(user_uid,[]):\n+ count = 0\n+ for s in self.users.get(user_uid, []):\n if await s.send_json(message):\n- count += 1 \n- return count \n+ count += 1\n+ return count\n \n async def broadcast(self, channel_uid, message):\n- count = 0\n- async for channel_member in self.app.services.channel_member.find(channel_uid=channel_uid):\n- await self.send_to_user(channel_member[\"user_uid\"],message)\n- return True \n- \n+ async for channel_member in self.app.services.channel_member.find(\n+ channel_uid=channel_uid\n+ ):\n+ await self.send_to_user(channel_member[\"user_uid\"], message)\n+ return True\n+\n async def delete(self, ws):\n for s in [sock for sock in self.sockets if sock.ws == ws]:\n await s.close()\n self.sockets.remove(s)\n- \n- \ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nindex 02707b0..6055403 100644\n--- a/src/snek/service/user.py\n+++ b/src/snek/service/user.py\n@@ -1,16 +1,18 @@\n+import pathlib\n+\n from snek.system import security\n from snek.system.service import BaseService\n \n \n class UserService(BaseService):\n mapper_name = \"user\"\n- \n+\n async def search(self, query, **kwargs):\n query = query.strip().lower()\n if not query:\n raise []\n results = []\n- async for result in self.find(username=dict(ilike='%' + query + '%'), **kwargs):\n+ async for result in self.find(username={\"ilike\": \"%\" + query + \"%\"}, **kwargs):\n results.append(result)\n return results\n \n@@ -23,17 +25,32 @@ class UserService(BaseService):\n return True\n \n async def save(self, user):\n- if not user['color']:\n- user['color'] = await self.services.util.random_light_hex_color()\n+ if not user[\"color\"]:\n+ user[\"color\"] = await self.services.util.random_light_hex_color()\n return await super().save(user)\n \n+ async def authenticate(self, username, password):\n+ print(username, password, flush=True)\n+ success = await self.validate_login(username, password)\n+ print(success, flush=True)\n+ if not success:\n+ return None\n+\n+ model = await self.get(username=username, deleted_at=None)\n+ return model\n+\n+ async def get_home_folder(self, user_uid):\n+ folder = pathlib.Path(f\"./drive/{user_uid}\")\n+ if not folder.exists():\n+ folder.mkdir(parents=True, exist_ok=True)\n+ return folder\n \n async def register(self, email, username, password):\n if await self.exists(username=username):\n raise Exception(\"User already exists.\")\n model = await self.new()\n model[\"nick\"] = username\n- model['color'] = await self.services.util.random_light_hex_color()\n+ model[\"color\"] = await self.services.util.random_light_hex_color()\n model.email.value = email\n model.username.value = username\n model.password.value = await security.hash(password)\ndiff --git a/src/snek/service/util.py b/src/snek/service/util.py\nindex 5550a8c..b620d9c 100644\n--- a/src/snek/service/util.py\n+++ b/src/snek/service/util.py\n@@ -1,15 +1,14 @@\n import random\n \n-\n from snek.system.service import BaseService\n \n \n class UtilService(BaseService):\n- \n+\n async def random_light_hex_color(self):\n- \n+\n r = random.randint(128, 255)\n g = random.randint(128, 255)\n b = random.randint(128, 255)\n- \n\\ No newline at end of file\n+\ndiff --git a/src/snek/system/cache.py b/src/snek/system/cache.py\nindex cd9484d..e97fdc3 100644\n--- a/src/snek/system/cache.py\n+++ b/src/snek/system/cache.py\n@@ -20,13 +20,13 @@ class Cache:\n try:\n self.lru.pop(self.lru.index(args))\n except:\n return None\n self.lru.insert(0, args)\n while len(self.lru) > self.max_items:\n self.cache.pop(self.lru[-1])\n self.lru.pop()\n return self.cache[args]\n \n def json_default(self, value):\n@@ -61,7 +61,7 @@ class Cache:\n \n if is_new:\n self.version += 1\n \n async def delete(self, args):\n if args in self.cache:\ndiff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py\nindex ca603d8..82a222e 100644\n--- a/src/snek/system/markdown.py\n+++ b/src/snek/system/markdown.py\n@@ -1,7 +1,7 @@\n \n from types import SimpleNamespace\n-from html import escape\n+\n from app.cache import time_cache_async\n from mistune import HTMLRenderer, Markdown\n from pygments import highlight\n@@ -24,21 +24,21 @@ class MarkdownRenderer(HTMLRenderer):\n def _escape(self, str):\n \n- def get_lexer(self, lang, default='bash'):\n+ def get_lexer(self, lang, default=\"bash\"):\n try:\n return get_lexer_by_name(lang, stripall=True)\n except:\n return get_lexer_by_name(default, stripall=True)\n- \n+\n def block_code(self, code, lang=None, info=None):\n if not lang:\n lang = info\n if not lang:\n- lang = 'bash'\n+ lang = \"bash\"\n lexer = self.get_lexer(lang)\n formatter = html.HtmlFormatter(lineseparator=\"
\")\n result = highlight(code, lexer, formatter)\n- return result \n+ return result\n \n def render(self):\n markdown_string = self.app.template_path.joinpath(self.template).read_text()\ndiff --git a/src/snek/system/middleware.py b/src/snek/system/middleware.py\nindex 2946262..1a6c7e6 100644\n--- a/src/snek/system/middleware.py\n+++ b/src/snek/system/middleware.py\n@@ -20,7 +20,9 @@ async def no_cors_middleware(request, handler):\n async def cors_allow_middleware(request, handler):\n response = await handler(request)\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n- response.headers[\"Access-Control-Allow-Methods\"] = \"GET, POST, OPTIONS, PUT, DELETE\"\n+ response.headers[\"Access-Control-Allow-Methods\"] = (\n+ \"GET, POST, OPTIONS, PUT, DELETE, MOVE, COPY, HEAD, LOCK, UNLOCK, PATCH, PROPFIND\"\n+ )\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n response.headers[\"Access-Control-Allow-Credentials\"] = \"true\"\n return response\n@@ -28,17 +30,12 @@ async def cors_allow_middleware(request, handler):\n \n @web.middleware\n async def cors_middleware(request, handler):\n- if request.method == \"OPTIONS\":\n- response = web.Response()\n- response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n- response.headers[\"Access-Control-Allow-Methods\"] = (\n- \"GET, POST, PUT, DELETE, OPTIONS\"\n- )\n- response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n- response.headers[\"Access-Control-Allow-Credentials\"] = \"true\"\n- return response\n+ if request.headers.get(\"Allow\"):\n+ return await handler(request)\n \n response = await handler(request)\n+ if request.headers.get(\"Allow\"):\n+ return response\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n response.headers[\"Access-Control-Allow-Methods\"] = \"GET, POST, PUT, DELETE, OPTIONS\"\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\ndiff --git a/src/snek/system/profiler.py b/src/snek/system/profiler.py\nindex 193bbb7..e0e5542 100644\n--- a/src/snek/system/profiler.py\n+++ b/src/snek/system/profiler.py\n@@ -1,24 +1,27 @@\n import cProfile\n import pstats\n-import sys \n+import sys\n+\n from aiohttp import web\n+\n profiler = None\n-import io \n+import io\n \n \n @web.middleware\n async def profile_middleware(request, handler):\n- global profiler \n+ global profiler\n if not profiler:\n profiler = cProfile.Profile()\n profiler.enable()\n response = await handler(request)\n profiler.disable()\n stats = pstats.Stats(profiler, stream=sys.stdout)\n- stats.sort_stats('cumulative')\n- stats.print_stats() \n+ stats.sort_stats(\"cumulative\")\n+ stats.print_stats()\n return response\n \n+\n async def profiler_handler(request):\n output = io.StringIO()\n stats = pstats.Stats(profiler, stream=output)\n@@ -27,17 +30,17 @@ async def profiler_handler(request):\n stats.print_stats()\n return web.Response(text=output.getvalue())\n \n+\n class Profiler:\n \n def __init__(self):\n- global profiler \n+ global profiler\n if profiler is None:\n profiler = cProfile.Profile()\n self.profiler = profiler\n \n async def __aenter__(self):\n- self.profiler.enable() \n- \n+ self.profiler.enable()\n+\n async def __aexit__(self, *args, **kwargs):\n self.profiler.disable()\n-\ndiff --git a/src/snek/system/service.py b/src/snek/system/service.py\nindex d65f947..c6d2afc 100644\n--- a/src/snek/system/service.py\n+++ b/src/snek/system/service.py\n@@ -58,7 +58,7 @@ class BaseService:\n raise Exception(f\"Couldn't save model. Errors: f{errors}\")\n \n async def find(self, **kwargs):\n- if not \"_limit\" in kwargs or int(kwargs.get(\"_limit\")) > 30:\n+ if \"_limit\" not in kwargs or int(kwargs.get(\"_limit\")) > 30:\n kwargs[\"_limit\"] = 60\n async for model in self.mapper.find(**kwargs):\n yield model\ndiff --git a/src/snek/system/template.py b/src/snek/system/template.py\nindex 0e71b80..cff807b 100644\n--- a/src/snek/system/template.py\n+++ b/src/snek/system/template.py\n@@ -1,15 +1,21 @@\n+import re\n from types import SimpleNamespace\n-from bs4 import BeautifulSoup\n-import re \n-import emoji\n \n+import emoji\n+from bs4 import BeautifulSoup\n from jinja2 import TemplateSyntaxError, nodes\n from jinja2.ext import Extension\n from jinja2.nodes import Const\n \n-emoji.EMOJI_DATA[''] = {\"en\": \":snek1:\",\"status\":2,\"E\":0.6, \"alias\":[\":snek1:\"]} \n+emoji.EMOJI_DATA[''] = {\n+ \"en\": \":snek1:\",\n+ \"status\": 2,\n+ \"E\": 0.6,\n+ \"alias\": [\":snek1:\"],\n+}\n \n-emoji.EMOJI_DATA[\"\"\"\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n+emoji.EMOJI_DATA[\n+ \"\"\"\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28c0\u28e0\u28e4\u2874\u2836\u28b6\u28de\u28db\u28db\u2873\u28f3\u2836\u28f6\u2876\u28b6\u28b6\u28e6\u28e4\u28c4\u28c0\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28e0\u28e4\u2836\u281a\u28eb\u28f4\u28ec\u2810\u28f6\u28ff\u28ff\u28cf\u28fd\u28ff\u28ff\u28c7\u28bf\u28ef\u28ff\u28ff\u28fb\u28ff\u28ff\u28fe\u28ee\u28f9\u28ff\u28b6\u28c4\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u28c0\u28f4\u28be\u28fb\u28fd\u28fe\u2847\u28a1\u28ff\u28ff\u28c7\u285f\u28ff\u28ff\u28ff\u28fc\u28ff\u28ff\u28ff\u28ff\u28b8\u28ff\u28df\u28ff\u28f7\u28bb\u28ff\u28ff\u28ff\u28f7\u287d\u28e7\u28f9\u287b\u28e6\u2840\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n@@ -64,62 +70,91 @@ emoji.EMOJI_DATA[\"\"\"\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u28f0\u28df\u28fd\u28ff\u28ef\u28ff\u28e3\u28ff\u28ff\u28ff\u28ff\u28ff\u2844\u28bf\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u287f\u281f\u28e1\u28fe\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u284f\u28f0\u28ff\u28ff\u287f\u28b9\u2847\u28fe\u2800\u28ff\u28ff\u28fe\u28ff\u2847\u2800\u2808\u28b7\u28b0\u28ff\u28b8\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28f7\u28fd\u28fb\u28bf\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28f7\u28e6\u28e6\u28c0\u2840\u2808\u281b\u283b\u28bf\u28ce\u28bf\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28f7\u28c4\u2800\u28ff\u2847\u2818\u28e7\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u28f4\u289f\u28fe\u28ff\u28fb\u287f\u28f5\u28ff\u28ff\u28ff\u28ff\u28df\u28fe\u28ef\u280c\u281b\u281b\u281b\u281b\u2809\u2801\u28a0\u28fe\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u287f\u280f\u28f0\u28ff\u28ff\u287f\u2801\u28b8\u2847\u28bf\u2800\u28ff\u28ff\u28ff\u28ff\u2847\u2800\u2820\u2808\u2802\u2883\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28fe\u28ff\u28fb\u287f\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u2847\u2844\u283e\u28f2\u28fe\u28ff\u28cc\u28bf\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28e7\u2859\u2807\u2800\u28bb\u2844\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u281b\u281a\u281b\u281b\u281b\u281a\u281b\u281b\u281b\u281b\u281b\u281a\u281b\u281b\u2813\u281a\u2812\u281b\u2812\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281a\u281b\u281b\u281b\u281b\u2813\u281b\u281b\u281b\u2813\u281b\u281b\u281b\u281b\u2813\u281a\u281b\u281b\u2813\u281a\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281a\u281b\u281b\u281b\u281b\u281b\u281b\u2813\u281b\u281a\u281b\u281b\u281b\u281b\u2813\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u2812\u281a\u281a\u2801\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n-\"\"\"] = {\"en\": \":a1:\",\"status\":2,\"E\":0.6, \"alias\":[\":a1:\"]}\n+\"\"\"\n+] = {\"en\": \":a1:\", \"status\": 2, \"E\": 0.6, \"alias\": [\":a1:\"]}\n+\n \n def set_link_target_blank(text):\n- soup = BeautifulSoup(text, 'html.parser')\n+ soup = BeautifulSoup(text, \"html.parser\")\n \n- for element in soup.find_all(\"a\"): \n- element.attrs['target'] = '_blank'\n- element.attrs['rel'] = 'noopener noreferrer'\n- element.attrs['referrerpolicy'] = 'no-referrer'\n- element.attrs['href'] = element.attrs['href'].strip(\".\").strip(\",\")\n+ for element in soup.find_all(\"a\"):\n+ element.attrs[\"target\"] = \"_blank\"\n+ element.attrs[\"rel\"] = \"noopener noreferrer\"\n+ element.attrs[\"referrerpolicy\"] = \"no-referrer\"\n+ element.attrs[\"href\"] = element.attrs[\"href\"].strip(\".\").strip(\",\")\n \n return str(soup)\n \n+\n def embed_youtube(text):\n- soup = BeautifulSoup(text, 'html.parser') \n- for element in soup.find_all(\"a\"): \n+ soup = BeautifulSoup(text, \"html.parser\")\n+ for element in soup.find_all(\"a\"):\n+ if (\n+ and \"?v=\" in element.attrs[\"href\"]\n+ ):\n video_name = element.attrs[\"href\"].split(\"?v=\")[1].split(\"&\")[0]\n- element.replace_with(BeautifulSoup(embed_template, 'html.parser'))\n+ element.replace_with(BeautifulSoup(embed_template, \"html.parser\"))\n return str(soup)\n \n+\n def embed_image(text):\n- soup = BeautifulSoup(text, 'html.parser') \n- for element in soup.find_all(\"a\"): \n- for extension in [\".png\", \".jpg\", \".jpeg\", \".gif\", \".webp\", \".svg\", \".bmp\", \".tiff\", \".ico\", \".heif\"]:\n- if extension in element.attrs['href'].lower():\n+ soup = BeautifulSoup(text, \"html.parser\")\n+ for element in soup.find_all(\"a\"):\n+ for extension in [\n+ \".png\",\n+ \".jpg\",\n+ \".jpeg\",\n+ \".gif\",\n+ \".webp\",\n+ \".svg\",\n+ \".bmp\",\n+ \".tiff\",\n+ \".ico\",\n+ \".heif\",\n+ ]:\n+ if extension in element.attrs[\"href\"].lower():\n embed_template = f'\"{element.attrs[\"href\"]}\"'\n- element.replace_with(BeautifulSoup(embed_template, 'html.parser'))\n+ element.replace_with(BeautifulSoup(embed_template, \"html.parser\"))\n return str(soup)\n \n+\n def embed_media(text):\n- soup = BeautifulSoup(text, 'html.parser') \n- for element in soup.find_all(\"a\"): \n- for extension in [\".mp4\", \".mp3\", \".wav\", \".ogg\", \".webm\", \".flac\", \".aac\",\".mpg\",\".avi\",\".wmv\"]:\n- if extension in element.attrs['href'].lower():\n+ soup = BeautifulSoup(text, \"html.parser\")\n+ for element in soup.find_all(\"a\"):\n+ for extension in [\n+ \".mp4\",\n+ \".mp3\",\n+ \".wav\",\n+ \".ogg\",\n+ \".webm\",\n+ \".flac\",\n+ \".aac\",\n+ \".mpg\",\n+ \".avi\",\n+ \".wmv\",\n+ ]:\n+ if extension in element.attrs[\"href\"].lower():\n embed_template = f''\n- element.replace_with(BeautifulSoup(embed_template, 'html.parser'))\n+ element.replace_with(BeautifulSoup(embed_template, \"html.parser\"))\n return str(soup)\n \n \n-\n def linkify_https(text):\n- return text \n+ return text\n \n- soup = BeautifulSoup(text, 'html.parser')\n+ soup = BeautifulSoup(text, \"html.parser\")\n \n- for element in soup.find_all(text=True): \n+ for element in soup.find_all(text=True):\n parent = element.parent\n- if parent.name in ['a', 'script', 'style']: \n+ if parent.name in [\"a\", \"script\", \"style\"]:\n continue\n- \n+\n new_text = re.sub(url_pattern, r'\">\\g<0>', element)\n- element.replace_with(BeautifulSoup(new_text, 'html.parser'))\n+ element.replace_with(BeautifulSoup(new_text, \"html.parser\"))\n \n return set_link_target_blank(str(soup))\n \n@@ -140,8 +175,7 @@ class EmojiExtension(Extension):\n ).set_lineno(line_number)\n \n def _to_html(self, md_file, caller):\n- return emoji.emojize(caller(),language='alias')\n-\n+ return emoji.emojize(caller(), language=\"alias\")\n \n \n class LinkifyExtension(Extension):\n@@ -170,6 +204,7 @@ class LinkifyExtension(Extension):\n result = embed_youtube(result)\n return result\n \n+\n class PythonExtension(Extension):\n tags = {\"py3\"}\n \n@@ -186,26 +221,26 @@ class PythonExtension(Extension):\n ).set_lineno(line_number)\n \n def _to_html(self, md_file, caller):\n- \n+\n def fn(source):\n- import subprocess \n- import subprocess \n- import pathlib \n- from pathlib import Path \n- import os \n- import sys \n- import requests\n+ import subprocess\n+\n def system(command):\n if isinstance(command):\n command = command.split(\" \")\n- from io import StringIO \n+ from io import StringIO\n+\n stdout = StringIO()\n- subprocess.run(command,stderr=stdout,stdout=stdout,text=True)\n+ subprocess.run(command, stderr=stdout, stdout=stdout, text=True)\n return stdout.getvalue()\n+\n to_write = []\n+\n def render(text):\n- global to_write \n+ global to_write\n to_write.append(text)\n+\n exec(source)\n return \"\".join(to_write)\n+\n return str(fn(caller()))\ndiff --git a/src/snek/system/terminal.py b/src/snek/system/terminal.py\nindex 80dda17..4d3781e 100644\n--- a/src/snek/system/terminal.py\n+++ b/src/snek/system/terminal.py\n@@ -1,30 +1,27 @@\n import asyncio\n-import aiohttp\n-import aiohttp.web\n import os\n import pty\n-import shlex\n import subprocess\n-import pathlib\n \n commands = {\n- 'alpine': 'docker run -it alpine /bin/sh',\n- 'r': 'docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh',\n+ \"alpine\": \"docker run -it alpine /bin/sh\",\n+ \"r\": \"docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh\",\n }\n \n+\n class TerminalSession:\n- def __init__(self,command):\n+ def __init__(self, command):\n self.master, self.slave = pty.openpty()\n- self.sockets =[]\n- self.history = b''\n- self.history_size = 1024*20\n+ self.sockets = []\n+ self.history = b\"\"\n+ self.history_size = 1024 * 20\n self.process = subprocess.Popen(\n command.split(\" \"),\n stdin=self.slave,\n stdout=self.slave,\n stderr=self.slave,\n bufsize=0,\n- universal_newlines=True\n+ universal_newlines=True,\n )\n \n async def add_websocket(self, ws):\n@@ -35,11 +32,11 @@ class TerminalSession:\n if len(self.sockets) > 1 and self.history:\n start = 0\n try:\n- start = self.history.index(b'\\n')\n+ start = self.history.index(b\"\\n\")\n except ValueError:\n- pass \n+ pass\n await ws.send_bytes(self.history[start:])\n- return \n+ return\n loop = asyncio.get_event_loop()\n while True:\n try:\n@@ -48,9 +45,10 @@ class TerminalSession:\n break\n self.history += data\n if len(self.history) > self.history_size:\n- self.history = self.history[:0-self.history_size]\n+ self.history = self.history[: 0 - self.history_size]\n try:\n+ for ws in self.sockets:\n except:\n self.sockets.remove(ws)\n except Exception:\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nindex 2765642..4a6e7a1 100644\n--- a/src/snek/system/view.py\n+++ b/src/snek/system/view.py\n@@ -12,9 +12,9 @@ class BaseView(web.View):\n return web.HTTPFound(\"/\")\n return await super()._iter()\n \n- @property \n+ @property\n def base_url(self):\n- return str(self.request.url.with_path('').with_query(''))\n+ return str(self.request.url.with_path(\"\").with_query(\"\"))\n \n @property\n def app(self):\ndiff --git a/src/snek/view/about.py b/src/snek/view/about.py\nindex b03f922..aba57ae 100644\n--- a/src/snek/view/about.py\n+++ b/src/snek/view/about.py\n@@ -26,12 +26,14 @@\n \n from snek.system.view import BaseView\n \n+\n class AboutHTMLView(BaseView):\n- \n+\n async def get(self):\n return await self.render_template(\"about.html\")\n \n+\n class AboutMDView(BaseView):\n \n async def get(self):\n- return await self.render_template(\"about.md\")\n\\ No newline at end of file\n+ return await self.render_template(\"about.md\")\ndiff --git a/src/snek/view/avatar.py b/src/snek/view/avatar.py\nindex cbd973c..a85b876 100644\n--- a/src/snek/view/avatar.py\n+++ b/src/snek/view/avatar.py\n@@ -23,11 +23,14 @@\n-from multiavatar import multiavatar\n import uuid\n+\n from aiohttp import web\n+from multiavatar import multiavatar\n+\n from snek.system.view import BaseView\n \n+\n class AvatarView(BaseView):\n login_required = False\n \n@@ -36,6 +39,6 @@ class AvatarView(BaseView):\n if uid == \"unique\":\n uid = str(uuid.uuid4())\n avatar = multiavatar.multiavatar(uid, True, None)\n- response = web.Response(text=avatar, content_type='image/svg+xml')\n- response.headers['Cache-Control'] = f'public, max-age={1337*42}'\n+ response = web.Response(text=avatar, content_type=\"image/svg+xml\")\n+ response.headers[\"Cache-Control\"] = f\"public, max-age={1337*42}\"\n return response\ndiff --git a/src/snek/view/docs.py b/src/snek/view/docs.py\nindex 592d1a2..bb63413 100644\n--- a/src/snek/view/docs.py\n+++ b/src/snek/view/docs.py\n@@ -1,35 +1,37 @@\n- \n- \n- \n- \n- \n+\n+\n+\n+\n+\n from snek.system.view import BaseView\n \n+\n class DocsHTMLView(BaseView):\n \n async def get(self):\n return await self.render_template(\"docs.html\")\n \n+\n class DocsMDView(BaseView):\n \n async def get(self):\n- return await self.render_template(\"docs.md\")\n\\ No newline at end of file\n+ return await self.render_template(\"docs.md\")\ndiff --git a/src/snek/view/drive.py b/src/snek/view/drive.py\nindex 1026cf7..853cdb2 100644\n--- a/src/snek/view/drive.py\n+++ b/src/snek/view/drive.py\n@@ -1,6 +1,8 @@\n-from snek.system.view import BaseView\n from aiohttp import web\n \n+from snek.system.view import BaseView\n+\n+\n class DriveView(BaseView):\n \n login_required = True\n@@ -14,21 +16,22 @@ class DriveView(BaseView):\n drive_items = []\n async for item in drive.items:\n record = item.record\n- record['url'] = '/drive.bin/' + record['uid'] + '.' + item.extension\n+ record[\"url\"] = \"/drive.bin/\" + record[\"uid\"] + \".\" + item.extension\n drive_items.append(record)\n return web.json_response(drive_items)\n \n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n \n- \n drives = []\n- async for drive in self.services.drive.get_by_user(user['uid']):\n+ async for drive in self.services.drive.get_by_user(user[\"uid\"]):\n record = drive.record\n- record['items'] = []\n+ record[\"items\"] = []\n async for item in drive.items:\n drive_item_record = item.record\n- drive_item_record['url'] = '/drive.bin/' + drive_item_record['uid'] + '.' + item.extension\n- record['items'].append(item.record)\n+ drive_item_record[\"url\"] = (\n+ \"/drive.bin/\" + drive_item_record[\"uid\"] + \".\" + item.extension\n+ )\n+ record[\"items\"].append(item.record)\n drives.append(record)\n- \n+\n return web.json_response(drives)\ndiff --git a/src/snek/view/index.py b/src/snek/view/index.py\nindex 3e62518..6ad6e70 100644\n--- a/src/snek/view/index.py\n+++ b/src/snek/view/index.py\n@@ -12,6 +12,7 @@\n \n from snek.system.view import BaseView\n \n+\n class IndexView(BaseView):\n async def get(self):\n- return await self.render_template(\"index.html\")\n\\ No newline at end of file\n+ return await self.render_template(\"index.html\")\ndiff --git a/src/snek/view/login.py b/src/snek/view/login.py\nindex 5028a7a..be79328 100644\n--- a/src/snek/view/login.py\n+++ b/src/snek/view/login.py\n@@ -7,9 +7,11 @@\n \n from aiohttp import web\n+\n from snek.form.login import LoginForm\n from snek.system.view import BaseFormView\n \n+\n class LoginView(BaseFormView):\n form = LoginForm\n \n@@ -18,17 +20,23 @@ class LoginView(BaseFormView):\n return web.HTTPFound(\"/web.html\")\n if self.request.path.endswith(\".json\"):\n return await super().get()\n- return await self.render_template(\"login.html\", {\"form\": await self.form(app=self.app).to_json()})\n+ return await self.render_template(\n+ \"login.html\", {\"form\": await self.form(app=self.app).to_json()}\n+ )\n \n async def submit(self, form):\n if await form.is_valid:\n- user = await self.services.user.get(username=form['username'], deleted_at=None)\n+ user = await self.services.user.get(\n+ username=form[\"username\"], deleted_at=None\n+ )\n await self.services.user.save(user)\n- self.session.update({\n- \"logged_in\": True,\n- \"username\": user['username'],\n- \"uid\": user[\"uid\"],\n- \"color\": user[\"color\"]\n- })\n+ self.session.update(\n+ {\n+ \"logged_in\": True,\n+ \"username\": user[\"username\"],\n+ \"uid\": user[\"uid\"],\n+ \"color\": user[\"color\"],\n+ }\n+ )\n return {\"redirect_url\": \"/web.html\"}\n- return {\"is_valid\": False}\n\\ No newline at end of file\n+ return {\"is_valid\": False}\ndiff --git a/src/snek/view/login_form.py b/src/snek/view/login_form.py\nindex 230d334..acf7c75 100644\n--- a/src/snek/view/login_form.py\n+++ b/src/snek/view/login_form.py\n@@ -20,4 +20,4 @@ class LoginFormView(BaseFormView):\n self.session[\"username\"] = form.username.value\n self.session[\"uid\"] = form.uid.value\n return {\"redirect_url\": \"/web.html\"}\n- return {\"is_valid\": False}\n\\ No newline at end of file\n+ return {\"is_valid\": False}\ndiff --git a/src/snek/view/logout.py b/src/snek/view/logout.py\nindex 57b92a3..42016d8 100644\n--- a/src/snek/view/logout.py\n+++ b/src/snek/view/logout.py\n@@ -15,10 +15,10 @@\n@@ -29,6 +29,7 @@\n \n \n from aiohttp import web\n+\n from snek.system.view import BaseView\n \n \n@@ -52,4 +53,4 @@ class LogoutView(BaseView):\n del self.session[\"username\"]\n except KeyError:\n pass\n- return await self.json_response({\"redirect_url\": self.redirect_url})\n\\ No newline at end of file\n+ return await self.json_response({\"redirect_url\": self.redirect_url})\ndiff --git a/src/snek/view/register.py b/src/snek/view/register.py\nindex 6e49506..7fbce9d 100644\n--- a/src/snek/view/register.py\n+++ b/src/snek/view/register.py\n@@ -2,14 +2,16 @@\n \n \n \n \n from aiohttp import web\n+\n from snek.form.register import RegisterForm\n from snek.system.view import BaseFormView\n \n+\n class RegisterView(BaseFormView):\n form = RegisterForm\n \n@@ -18,16 +20,20 @@ class RegisterView(BaseFormView):\n return web.HTTPFound(\"/web.html\")\n if self.request.path.endswith(\".json\"):\n return await super().get()\n- return await self.render_template(\"register.html\", {\"form\": await self.form(app=self.app).to_json()})\n+ return await self.render_template(\n+ \"register.html\", {\"form\": await self.form(app=self.app).to_json()}\n+ )\n \n async def submit(self, form):\n result = await self.app.services.user.register(\n form.email.value, form.username.value, form.password.value\n )\n- self.request.session.update({\n- \"uid\": result[\"uid\"],\n- \"username\": result[\"username\"],\n- \"logged_in\": True,\n- \"color\": result[\"color\"]\n- })\n- return {\"redirect_url\": \"/web.html\"}\n\\ No newline at end of file\n+ self.request.session.update(\n+ {\n+ \"uid\": result[\"uid\"],\n+ \"username\": result[\"username\"],\n+ \"logged_in\": True,\n+ \"color\": result[\"color\"],\n+ }\n+ )\n+ return {\"redirect_url\": \"/web.html\"}\ndiff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py\nindex 5ce42a7..7b98647 100644\n--- a/src/snek/view/register_form.py\n+++ b/src/snek/view/register_form.py\n@@ -2,7 +2,7 @@\n \n \n \n@@ -13,10 +13,10 @@\n@@ -28,6 +28,7 @@\n from snek.form.register import RegisterForm\n from snek.system.view import BaseFormView\n \n+\n class RegisterFormView(BaseFormView):\n form = RegisterForm\n \n@@ -35,10 +36,12 @@ class RegisterFormView(BaseFormView):\n result = await self.app.services.user.register(\n form.email.value, form.username.value, form.password.value\n )\n- self.request.session.update({\n- \"uid\": result[\"uid\"],\n- \"username\": result[\"username\"],\n- \"logged_in\": True,\n- \"color\": result[\"color\"]\n- })\n- return {\"redirect_url\": \"/web.html\"}\n\\ No newline at end of file\n+ self.request.session.update(\n+ {\n+ \"uid\": result[\"uid\"],\n+ \"username\": result[\"username\"],\n+ \"logged_in\": True,\n+ \"color\": result[\"color\"],\n+ }\n+ )\n+ return {\"redirect_url\": \"/web.html\"}\ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex 27a8dcf..19c98d4 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -7,41 +7,44 @@\n \n \n-from aiohttp import web \n-from snek.system.view import BaseView\n-import traceback\n import json\n-from snek.system.model import now \n+import traceback\n+\n+from aiohttp import web\n+\n+from snek.system.model import now\n from snek.system.profiler import Profiler\n+from snek.system.view import BaseView\n+\n \n class RPCView(BaseView):\n \n class RPCApi:\n def __init__(self, view, ws):\n- self.view = view \n+ self.view = view\n self.app = self.view.app\n self.services = self.app.services\n- self.ws = ws \n+ self.ws = ws\n \n @property\n def user_uid(self):\n return self.view.session.get(\"uid\")\n \n- @property \n+ @property\n def request(self):\n- return self.view.request \n- \n+ return self.view.request\n+\n def _require_login(self):\n if not self.is_logged_in:\n raise Exception(\"Not logged in\")\n \n- @property \n+ @property\n def is_logged_in(self):\n return self.view.session.get(\"logged_in\", False)\n \n async def mark_as_read(self, channel_uid):\n self._require_login()\n- await self.services.channel_member.mark_as_read(channel_uid, self.user_uid) \n+ await self.services.channel_member.mark_as_read(channel_uid, self.user_uid)\n return True\n \n async def login(self, username, password):\n@@ -54,16 +57,26 @@ class RPCView(BaseView):\n self.view.session[\"username\"] = user[\"username\"]\n self.view.session[\"user_nick\"] = user[\"nick\"]\n record = user.record\n- del record['password']\n- del record['deleted_at']\n- await self.services.socket.add(self.ws,self.view.request.session.get('uid'))\n- async for subscription in self.services.channel_member.find(user_uid=self.view.request.session.get(\"uid\"), deleted_at=None, is_banned=False):\n- await self.services.socket.subscribe(self.ws, subscription[\"channel_uid\"], self.view.request.session.get(\"uid\"))\n- return record \n-\n- async def search_user(self, query): \n+ del record[\"password\"]\n+ del record[\"deleted_at\"]\n+ await self.services.socket.add(\n+ self.ws, self.view.request.session.get(\"uid\")\n+ )\n+ async for subscription in self.services.channel_member.find(\n+ user_uid=self.view.request.session.get(\"uid\"),\n+ deleted_at=None,\n+ is_banned=False,\n+ ):\n+ await self.services.socket.subscribe(\n+ self.ws,\n+ subscription[\"channel_uid\"],\n+ self.view.request.session.get(\"uid\"),\n+ )\n+ return record\n+\n+ async def search_user(self, query):\n self._require_login()\n- return [user['username'] for user in await self.services.user.search(query)]\n+ return [user[\"username\"] for user in await self.services.user.search(query)]\n \n async def get_user(self, user_uid):\n self._require_login()\n@@ -71,46 +84,56 @@ class RPCView(BaseView):\n user_uid = self.user_uid\n user = await self.services.user.get(uid=user_uid)\n record = user.record\n- del record['password']\n- del record['deleted_at']\n+ del record[\"password\"]\n+ del record[\"deleted_at\"]\n if user_uid != user[\"uid\"]:\n- del record['email']\n- return record \n+ del record[\"email\"]\n+ return record\n \n- async def get_messages(self, channel_uid, offset=0,timestamp = None):\n+ async def get_messages(self, channel_uid, offset=0, timestamp=None):\n self._require_login()\n messages = []\n- for message in await self.services.channel_message.offset(channel_uid, offset or 0,timestamp or None):\n- extended_dict = await self.services.channel_message.to_extended_dict(message)\n+ for message in await self.services.channel_message.offset(\n+ channel_uid, offset or 0, timestamp or None\n+ ):\n+ extended_dict = await self.services.channel_message.to_extended_dict(\n+ message\n+ )\n messages.append(extended_dict)\n return messages\n \n async def get_channels(self):\n self._require_login()\n channels = []\n- async for subscription in self.services.channel_member.find(user_uid=self.user_uid, is_banned=False):\n- channel = await self.services.channel.get(uid=subscription['channel_uid'])\n+ async for subscription in self.services.channel_member.find(\n+ user_uid=self.user_uid, is_banned=False\n+ ):\n+ channel = await self.services.channel.get(\n+ uid=subscription[\"channel_uid\"]\n+ )\n last_message = await channel.get_last_message()\n- color = None \n+ color = None\n if last_message:\n last_message_user = await last_message.get_user()\n- color = last_message_user['color']\n- channels.append({\n- \"name\": subscription[\"label\"],\n- \"uid\": subscription[\"channel_uid\"],\n- \"tag\": channel[\"tag\"],\n- \"new_count\": subscription[\"new_count\"],\n- \"is_moderator\": subscription[\"is_moderator\"],\n- \"is_read_only\": subscription[\"is_read_only\"],\n- 'new_count': subscription['new_count'],\n- 'color': color \n- })\n+ color = last_message_user[\"color\"]\n+ channels.append(\n+ {\n+ \"name\": subscription[\"label\"],\n+ \"uid\": subscription[\"channel_uid\"],\n+ \"tag\": channel[\"tag\"],\n+ \"new_count\": subscription[\"new_count\"],\n+ \"is_moderator\": subscription[\"is_moderator\"],\n+ \"is_read_only\": subscription[\"is_read_only\"],\n+ \"new_count\": subscription[\"new_count\"],\n+ \"color\": color,\n+ }\n+ )\n return channels\n \n async def send_message(self, channel_uid, message):\n self._require_login()\n await self.services.chat.send(self.user_uid, channel_uid, message)\n- return True \n+ return True\n \n async def echo(self, *args):\n self._require_login()\n@@ -118,29 +141,47 @@ class RPCView(BaseView):\n \n async def query(self, *args):\n self._require_login()\n- query = args[0] \n+ query = args[0]\n lowercase = query.lower()\n- if any(keyword in lowercase for keyword in [\"drop\", \"alter\", \"update\", \"delete\", \"replace\", \"insert\", \"truncate\"]) and 'select' not in lowercase:\n+ if (\n+ any(\n+ keyword in lowercase\n+ for keyword in [\n+ \"drop\",\n+ \"alter\",\n+ \"update\",\n+ \"delete\",\n+ \"replace\",\n+ \"insert\",\n+ \"truncate\",\n+ ]\n+ )\n+ and \"select\" not in lowercase\n+ ):\n raise Exception(\"Not allowed\")\n- records = [dict(record) async for record in self.services.channel.query(args[0])]\n+ records = [\n+ dict(record) async for record in self.services.channel.query(args[0])\n+ ]\n for record in records:\n try:\n- del record['email']\n+ del record[\"email\"]\n except KeyError:\n- pass \n+ pass\n try:\n del record[\"password\"]\n except KeyError:\n- pass \n+ pass\n try:\n- del record['message']\n+ del record[\"message\"]\n except:\n pass\n try:\n- del record['html']\n- except: \n+ del record[\"html\"]\n+ except:\n pass\n- return [dict(record) async for record in self.services.channel.query(args[0])]\n+ return [\n+ dict(record) async for record in self.services.channel.query(args[0])\n+ ]\n \n async def __call__(self, data):\n try:\n@@ -150,30 +191,43 @@ class RPCView(BaseView):\n raise Exception(\"Not allowed\")\n args = data.get(\"args\") or []\n if hasattr(super(), method_name) or not hasattr(self, method_name):\n- return await self._send_json({\"callId\": call_id, \"data\": \"Not allowed\"})\n+ return await self._send_json(\n+ {\"callId\": call_id, \"data\": \"Not allowed\"}\n+ )\n method = getattr(self, method_name.replace(\".\", \"_\"), None)\n if not method:\n raise Exception(\"Method not found\")\n- success = True \n+ success = True\n try:\n result = await method(*args)\n except Exception as ex:\n result = {\"exception\": str(ex), \"traceback\": traceback.format_exc()}\n success = False\n if result != \"noresponse\":\n- await self._send_json({\"callId\": call_id, \"success\": success, \"data\": result})\n+ await self._send_json(\n+ {\"callId\": call_id, \"success\": success, \"data\": result}\n+ )\n except Exception as ex:\n print(str(ex), flush=True)\n- await self._send_json({\"callId\": call_id, \"success\": False, \"data\": str(ex)})\n+ await self._send_json(\n+ {\"callId\": call_id, \"success\": False, \"data\": str(ex)}\n+ )\n \n async def _send_json(self, obj):\n await self.ws.send_str(json.dumps(obj, default=str))\n- \n \n async def get_online_users(self, channel_uid):\n self._require_login()\n- \n- 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)]\n+\n+ return [\n+ {\n+ \"uid\": record[\"uid\"],\n+ \"username\": record[\"username\"],\n+ \"nick\": record[\"nick\"],\n+ \"last_ping\": record[\"last_ping\"],\n+ }\n+ async for record in self.services.channel.get_online_users(channel_uid)\n+ ]\n \n async def echo(self, obj):\n await self.ws.send_json(obj)\n@@ -182,12 +236,20 @@ class RPCView(BaseView):\n async def get_users(self, channel_uid):\n self._require_login()\n \n- 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)]\n+ return [\n+ {\n+ \"uid\": record[\"uid\"],\n+ \"username\": record[\"username\"],\n+ \"nick\": record[\"nick\"],\n+ \"last_ping\": record[\"last_ping\"],\n+ }\n+ async for record in self.services.channel.get_users(channel_uid)\n+ ]\n \n async def ping(self, callId, *args):\n if self.user_uid:\n user = await self.services.user.get(uid=self.user_uid)\n- user['last_ping'] = now()\n+ user[\"last_ping\"] = now()\n await self.services.user.save(user)\n return {\"pong\": args}\n \n@@ -196,8 +258,14 @@ class RPCView(BaseView):\n await ws.prepare(self.request)\n if self.request.session.get(\"logged_in\"):\n await self.services.socket.add(ws, self.request.session.get(\"uid\"))\n- async for subscription in self.services.channel_member.find(user_uid=self.request.session.get(\"uid\"), deleted_at=None, is_banned=False):\n- await self.services.socket.subscribe(ws, subscription[\"channel_uid\"], self.request.session.get(\"uid\"))\n+ async for subscription in self.services.channel_member.find(\n+ user_uid=self.request.session.get(\"uid\"),\n+ deleted_at=None,\n+ is_banned=False,\n+ ):\n+ await self.services.socket.subscribe(\n+ ws, subscription[\"channel_uid\"], self.request.session.get(\"uid\")\n+ )\n rpc = RPCView.RPCApi(self, ws)\n async for msg in ws:\n if msg.type == web.WSMsgType.TEXT:\n@@ -209,7 +277,7 @@ class RPCView(BaseView):\n await self.services.socket.delete(ws)\n break\n elif msg.type == web.WSMsgType.ERROR:\n- pass \n+ pass\n elif msg.type == web.WSMsgType.CLOSE:\n- pass \n+ pass\n return ws\ndiff --git a/src/snek/view/search_user.py b/src/snek/view/search_user.py\nindex 347d3b2..d97a4b6 100644\n--- a/src/snek/view/search_user.py\n+++ b/src/snek/view/search_user.py\n@@ -8,17 +8,17 @@\n \n@@ -28,7 +28,6 @@\n \n \n-from aiohttp import web\n from snek.form.search_user import SearchUserForm\n from snek.system.view import BaseFormView\n \n@@ -40,14 +39,17 @@ class SearchUserView(BaseFormView):\n users = []\n query = self.request.query.get(\"query\")\n if query:\n- users = [user.record for user in await self.app.services.user.search(query)]\n+ users = [user.record for user in await self.app.services.user.search(query)]\n \n if self.request.path.endswith(\".json\"):\n return await super().get()\n current_user = await self.app.services.user.get(uid=self.session.get(\"uid\"))\n- return await self.render_template(\"search_user.html\", {\"users\": users, \"query\": query or '','current_user': current_user})\n+ return await self.render_template(\n+ \"search_user.html\",\n+ {\"users\": users, \"query\": query or \"\", \"current_user\": current_user},\n+ )\n \n async def submit(self, form):\n if await form.is_valid:\n- return {\"redirect_url\": \"/search-user.html?query=\" + form['username']}\n+ return {\"redirect_url\": \"/search-user.html?query=\" + form[\"username\"]}\n return {\"is_valid\": False}\ndiff --git a/src/snek/view/status.py b/src/snek/view/status.py\nindex 117942a..4675572 100644\n--- a/src/snek/view/status.py\n+++ b/src/snek/view/status.py\n@@ -25,17 +25,18 @@\n \n from snek.system.view import BaseView\n \n+\n class StatusView(BaseView):\n async def get(self):\n memberships = []\n user = {}\n- \n+\n user_id = self.session.get(\"uid\")\n if user_id:\n user = await self.app.services.user.get(uid=user_id)\n if not user:\n return await self.json_response({\"error\": \"User not found\"}, status=404)\n- \n+\n async for model in self.app.services.channel_member.find(\n user_uid=user_id, deleted_at=None, is_banned=False\n ):\n@@ -69,4 +70,4 @@ class StatusView(BaseView):\n self.app.cache.cache, None\n ),\n }\n- )\n\\ No newline at end of file\n+ )\ndiff --git a/src/snek/view/terminal.py b/src/snek/view/terminal.py\nindex 6596f2c..d3af9b0 100644\n--- a/src/snek/view/terminal.py\n+++ b/src/snek/view/terminal.py\n@@ -1,15 +1,17 @@\n-from snek.system.view import BaseView \n-import aiohttp \n-import asyncio\n-from snek.system.terminal import TerminalSession\n import pathlib\n \n+import aiohttp\n+\n+from snek.system.terminal import TerminalSession\n+from snek.system.view import BaseView\n+\n+\n class TerminalSocketView(BaseView):\n- \n+\n login_required = True\n \n user_sessions = {}\n- \n+\n async def prepare_drive(self):\n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n root = pathlib.Path(\"drive\").joinpath(user[\"uid\"])\n@@ -19,40 +21,34 @@ class TerminalSocketView(BaseView):\n destination_path = root.joinpath(path.name)\n if not path.is_dir():\n destination_path.write_bytes(path.read_bytes())\n- return root \n- \n- async def get(self):\n- \n+ return root\n \n+ async def get(self):\n \n ws = aiohttp.web.WebSocketResponse()\n await ws.prepare(self.request)\n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n root = await self.prepare_drive()\n- \n-\n- \n \n command = f\"docker run -v ./{root}/:/root -it --memory 512M --cpus=0.5 -w /root snek_ubuntu /bin/bash\"\n \n session = self.user_sessions.get(user[\"uid\"])\n if not session:\n self.user_sessions[user[\"uid\"]] = TerminalSession(command=command)\n- session = self.user_sessions[user[\"uid\"]] \n+ session = self.user_sessions[user[\"uid\"]]\n await session.add_websocket(ws)\n \n async for msg in ws:\n if msg.type == aiohttp.WSMsgType.BINARY:\n await session.write_input(msg.data.decode())\n \n- \n return ws\n \n+\n class TerminalView(BaseView):\n \n login_required = True\n \n async def get(self):\n- request = self.request\n- return await self.request.app.render_template('terminal.html',self.request)\n+ return await self.request.app.render_template(\"terminal.html\", self.request)\ndiff --git a/src/snek/view/threads.py b/src/snek/view/threads.py\nindex 4f3fe8c..bc923c6 100644\n--- a/src/snek/view/threads.py\n+++ b/src/snek/view/threads.py\n@@ -1,5 +1,6 @@\n from snek.system.view import BaseView\n \n+\n class ThreadsView(BaseView):\n \n async def get(self):\n@@ -12,22 +13,25 @@ class ThreadsView(BaseView):\n if not last_message:\n continue\n \n- thread[\"uid\"] = channel['uid']\n+ thread[\"uid\"] = channel[\"uid\"]\n thread[\"name\"] = await channel_member.get_name()\n thread[\"new_count\"] = channel_member[\"new_count\"]\n thread[\"last_message_on\"] = channel[\"last_message_on\"]\n- thread['created_at'] = thread['last_message_on']\n+ thread[\"created_at\"] = thread[\"last_message_on\"]\n \n- \n thread[\"last_message_text\"] = last_message[\"message\"]\n- thread['last_message_user_uid'] = last_message[\"user_uid\"]\n- user_last_message = await self.app.services.user.get(uid=last_message[\"user_uid\"])\n- if channel['tag'] == \"dm\":\n- thread['name_color'] = user_last_message['color']\n- thread['last_message_user_color'] = user_last_message['color'] \n+ thread[\"last_message_user_uid\"] = last_message[\"user_uid\"]\n+ user_last_message = await self.app.services.user.get(\n+ uid=last_message[\"user_uid\"]\n+ )\n+ if channel[\"tag\"] == \"dm\":\n+ thread[\"name_color\"] = user_last_message[\"color\"]\n+ thread[\"last_message_user_color\"] = user_last_message[\"color\"]\n threads.append(thread)\n- \n- threads.sort(key=lambda x: x['last_message_on'] or '', reverse=True)\n \n- return await self.render_template(\"threads.html\", dict(threads=threads,user=user))\n+ threads.sort(key=lambda x: x[\"last_message_on\"] or \"\", reverse=True)\n+\n+ return await self.render_template(\n+ \"threads.html\", {\"threads\": threads, \"user\": user}\n+ )\ndiff --git a/src/snek/view/upload.py b/src/snek/view/upload.py\nindex 8884d72..b32d94e 100644\n--- a/src/snek/view/upload.py\n+++ b/src/snek/view/upload.py\n@@ -1,31 +1,36 @@\n \n \n \n \n-from snek.system.view import BaseView\n-import aiofiles\n import pathlib\n-from aiohttp import web\n import uuid\n \n+import aiofiles\n+from aiohttp import web\n+\n+from snek.system.view import BaseView\n+\n UPLOAD_DIR = pathlib.Path(\"./drive\")\n \n+\n class UploadView(BaseView):\n \n async def get(self):\n uid = self.request.match_info.get(\"uid\")\n drive_item = await self.services.drive_item.get(uid)\n response = web.FileResponse(drive_item[\"path\"])\n- response.headers['Cache-Control'] = f'public, max-age={1337*420}'\n- response.headers['Content-Disposition'] = f'attachment; filename=\"{drive_item[\"name\"]}\"'\n+ response.headers[\"Cache-Control\"] = f\"public, max-age={1337*420}\"\n+ response.headers[\"Content-Disposition\"] = (\n+ f'attachment; filename=\"{drive_item[\"name\"]}\"'\n+ )\n return response\n \n async def post(self):\n@@ -36,7 +41,9 @@ class UploadView(BaseView):\n \n channel_uid = None\n \n- drive = await self.services.drive.get_or_create(user_uid=self.request.session.get(\"uid\"))\n+ drive = await self.services.drive.get_or_create(\n+ user_uid=self.request.session.get(\"uid\")\n+ )\n \n extension_types = {\n \".jpg\": \"image\",\n@@ -47,7 +54,7 @@ class UploadView(BaseView):\n \".mp3\": \"audio\",\n \".pdf\": \"document\",\n \".doc\": \"document\",\n- \".docx\": \"document\"\n+ \".docx\": \"document\",\n }\n \n while field := await reader.next():\n@@ -58,32 +65,45 @@ class UploadView(BaseView):\n filename = field.filename\n if not filename:\n continue\n- \n+\n name = str(uuid.uuid4()) + pathlib.Path(filename).suffix\n \n file_path = pathlib.Path(UPLOAD_DIR).joinpath(name)\n files.append(file_path)\n \n- async with aiofiles.open(str(file_path.absolute()), 'wb') as f:\n+ async with aiofiles.open(str(file_path.absolute()), \"wb\") as f:\n while chunk := await field.read_chunk():\n await f.write(chunk)\n \n drive_item = await self.services.drive_item.create(\n- drive[\"uid\"], filename, str(file_path.absolute()), file_path.stat().st_size, file_path.suffix\n+ drive[\"uid\"],\n+ filename,\n+ str(file_path.absolute()),\n+ file_path.stat().st_size,\n+ file_path.suffix,\n )\n- \n- type_ = \"unknown\"\n+\n extension = \".\" + filename.split(\".\")[-1]\n if extension in extension_types:\n- type_ = extension_types[extension] \n- \n+ extension_types[extension]\n+\n await self.services.drive_item.save(drive_item)\n- response = \"Uploaded [\" + filename + \"](/drive.bin/\" + drive_item[\"uid\"] + \")\"\n- response = \"[\" + filename + \"](/drive.bin/\" + drive_item[\"uid\"] + extension + \")\"\n+ response = (\n+ \"Uploaded [\" + filename + \"](/drive.bin/\" + drive_item[\"uid\"] + \")\"\n+ )\n+ response = (\n+ \"[\" + filename + \"](/drive.bin/\" + drive_item[\"uid\"] + extension + \")\"\n+ )\n \n await self.services.chat.send(\n self.request.session.get(\"uid\"), channel_uid, response\n )\n \n- return web.json_response({\"message\": \"Files uploaded successfully\", \"files\": [str(file) for file in files], \"channel_uid\": channel_uid})\n+ return web.json_response(\n+ {\n+ \"message\": \"Files uploaded successfully\",\n+ \"files\": [str(file) for file in files],\n+ \"channel_uid\": channel_uid,\n+ }\n+ )\ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex b9271c3..111f76c 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -25,37 +25,55 @@\n \n from aiohttp import web\n+\n from snek.system.view import BaseView\n \n+\n class WebView(BaseView):\n login_required = True\n \n async def get(self):\n if self.login_required and not self.session.get(\"logged_in\"):\n return web.HTTPFound(\"/\")\n- channel = await self.services.channel.get(uid=self.request.match_info.get(\"channel\"))\n+ channel = await self.services.channel.get(\n+ uid=self.request.match_info.get(\"channel\")\n+ )\n if not channel:\n- user = await self.services.user.get(uid=self.request.match_info.get(\"channel\"))\n+ user = await self.services.user.get(\n+ uid=self.request.match_info.get(\"channel\")\n+ )\n if user:\n- channel = await self.services.channel.get_dm(self.session.get(\"uid\"), user[\"uid\"])\n+ channel = await self.services.channel.get_dm(\n+ self.session.get(\"uid\"), user[\"uid\"]\n+ )\n if channel:\n return web.HTTPFound(\"/channel/{}.html\".format(channel[\"uid\"]))\n if not channel:\n return web.HTTPNotFound()\n- \n- channel_member = await self.app.services.channel_member.get(user_uid=self.session.get(\"uid\"), channel_uid=channel[\"uid\"])\n+\n+ channel_member = await self.app.services.channel_member.get(\n+ user_uid=self.session.get(\"uid\"), channel_uid=channel[\"uid\"]\n+ )\n if not channel_member:\n return web.HTTPNotFound()\n- \n- channel_member['new_count'] = 0\n+\n+ channel_member[\"new_count\"] = 0\n await self.app.services.channel_member.save(channel_member)\n \n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n- messages = [await self.app.services.channel_message.to_extended_dict(message) for message in await self.app.services.channel_message.offset(\n- channel[\"uid\"]\n- )]\n+ messages = [\n+ await self.app.services.channel_message.to_extended_dict(message)\n+ for message in await self.app.services.channel_message.offset(\n+ channel[\"uid\"]\n+ )\n+ ]\n for message in messages:\n- await self.app.services.notification.mark_as_read(self.session.get(\"uid\"),message[\"uid\"])\n+ await self.app.services.notification.mark_as_read(\n+ self.session.get(\"uid\"), message[\"uid\"]\n+ )\n \n name = await channel_member.get_name()\n- return await self.render_template(\"web.html\", {\"name\": name, \"channel\": channel,\"user\": user,\"messages\": messages})\n+ return await self.render_template(\n+ \"web.html\",\n+ {\"name\": name, \"channel\": channel, \"user\": user, \"messages\": messages},\n+ )\ndiff --git a/src/snekssh/app.py b/src/snekssh/app.py\nindex 27e1b4b..aab17e4 100644\n--- a/src/snekssh/app.py\n+++ b/src/snekssh/app.py\n@@ -1,7 +1,9 @@\n import asyncio\n-import asyncssh\n-import os\n import logging\n+import os\n+\n+import asyncssh\n+\n asyncssh.set_debug_level(2)\n logging.basicConfig(level=logging.DEBUG)\n@@ -11,6 +13,7 @@ PASSWORD = \"woeii\"\n HOST = \"localhost\"\n PORT = 2225\n \n+\n class MySFTPServer(asyncssh.SFTPServer):\n def __init__(self, chan):\n super().__init__(chan)\n@@ -31,8 +34,10 @@ class MySFTPServer(asyncssh.SFTPServer):\n full_path = os.path.join(self.root, path.lstrip(\"/\"))\n return await super().listdir(full_path)\n \n+\n class MySSHServer(asyncssh.SSHServer):\n \"\"\"Custom SSH server to handle authentication\"\"\"\n+\n def connection_made(self, conn):\n print(f\"New connection from {conn.get_extra_info('peername')}\")\n \n@@ -46,11 +51,12 @@ class MySSHServer(asyncssh.SSHServer):\n \n def validate_password(self, username, password):\n- print(username,password)\n- \n+ print(username, password)\n+\n return True\n return username == USERNAME and password == PASSWORD\n \n+\n async def start_sftp_server():\n \n@@ -59,11 +65,12 @@ async def start_sftp_server():\n host=HOST,\n port=PORT,\n server_host_keys=[\"ssh_host_key\"],\n- process_factory=MySFTPServer\n+ process_factory=MySFTPServer,\n )\n print(f\"SFTP server running on {HOST}:{PORT}\")\n \n+\n if __name__ == \"__main__\":\n try:\n asyncio.run(start_sftp_server())\ndiff --git a/src/snekssh/app2.py b/src/snekssh/app2.py\nindex 1bfec21..2fa26a7 100644\n--- a/src/snekssh/app2.py\n+++ b/src/snekssh/app2.py\n@@ -1,7 +1,8 @@\n import asyncio\n-import asyncssh\n import os\n \n+import asyncssh\n+\n HOST = \"0.0.0.0\"\n PORT = 2225\n@@ -9,6 +10,7 @@ USERNAME = \"user\"\n PASSWORD = \"password\"\n \n+\n class CustomSSHServer(asyncssh.SSHServer):\n def connection_made(self, conn):\n print(f\"New connection from {conn.get_extra_info('peername')}\")\n@@ -22,6 +24,7 @@ class CustomSSHServer(asyncssh.SSHServer):\n def validate_password(self, username, password):\n return username == USERNAME and password == PASSWORD\n \n+\n async def custom_bash_process(process):\n \"\"\"Spawns a custom bash shell process\"\"\"\n env = os.environ.copy()\n@@ -29,7 +32,12 @@ async def custom_bash_process(process):\n \n bash_proc = await asyncio.create_subprocess_exec(\n- SHELL, \"-i\", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=env\n+ SHELL,\n+ \"-i\",\n+ stdin=asyncio.subprocess.PIPE,\n+ stdout=asyncio.subprocess.PIPE,\n+ stderr=asyncio.subprocess.PIPE,\n+ env=env,\n )\n \n async def read_output():\n@@ -48,6 +56,7 @@ async def custom_bash_process(process):\n \n await asyncio.gather(read_output(), read_input())\n \n+\n async def start_ssh_server():\n \"\"\"Starts the AsyncSSH server with Bash\"\"\"\n await asyncssh.create_server(\n@@ -55,14 +64,14 @@ async def start_ssh_server():\n host=HOST,\n port=PORT,\n server_host_keys=[\"ssh_host_key\"],\n- process_factory=custom_bash_process\n+ process_factory=custom_bash_process,\n )\n print(f\"SSH server running on {HOST}:{PORT}\")\n \n+\n if __name__ == \"__main__\":\n try:\n asyncio.run(start_ssh_server())\n except (OSError, asyncssh.Error) as e:\n print(f\"Error starting SSH server: {e}\")\n-\ndiff --git a/src/snekssh/app3.py b/src/snekssh/app3.py\nindex d50cc54..4a09452 100644\n--- a/src/snekssh/app3.py\n+++ b/src/snekssh/app3.py\n@@ -27,45 +27,48 @@\n \n-import asyncio, asyncssh, sys\n-\n-async def handle_client(process: asyncssh.SSHServerProcess) -> None:\n-\n+import asyncio\n+import sys\n \n+import asyncssh\n \n \n+async def handle_client(process: asyncssh.SSHServerProcess) -> None:\n \n width, height, pixwidth, pixheight = process.term_size\n \n- process.stdout.write(f'Terminal type: {process.term_type}, '\n- f'size: {width}x{height}')\n+ process.stdout.write(\n+ f\"Terminal type: {process.term_type}, \" f\"size: {width}x{height}\"\n+ )\n if pixwidth and pixheight:\n- process.stdout.write(f' ({pixwidth}x{pixheight} pixels)')\n- process.stdout.write('\\nTry resizing your window!\\n')\n+ process.stdout.write(f\" ({pixwidth}x{pixheight} pixels)\")\n+ process.stdout.write(\"\\nTry resizing your window!\\n\")\n \n while not process.stdin.at_eof():\n try:\n await process.stdin.read()\n except asyncssh.TerminalSizeChanged as exc:\n- process.stdout.write(f'New window size: {exc.width}x{exc.height}')\n+ process.stdout.write(f\"New window size: {exc.width}x{exc.height}\")\n if exc.pixwidth and exc.pixheight:\n- process.stdout.write(f' ({exc.pixwidth}'\n- f'x{exc.pixheight} pixels)')\n- process.stdout.write('\\n')\n-\n-\n+ process.stdout.write(f\" ({exc.pixwidth}\" f\"x{exc.pixheight} pixels)\")\n+ process.stdout.write(\"\\n\")\n \n \n async def start_server() -> None:\n- await asyncssh.listen('', 2230, server_host_keys=['ssh_host_key'],\n- process_factory=handle_client)\n+ await asyncssh.listen(\n+ \"\",\n+ 2230,\n+ server_host_keys=[\"ssh_host_key\"],\n+ process_factory=handle_client,\n+ )\n+\n \n loop = asyncio.new_event_loop()\n \n try:\n loop.run_until_complete(start_server())\n except (OSError, asyncssh.Error) as exc:\n- sys.exit('Error starting server: ' + str(exc))\n+ sys.exit(\"Error starting server: \" + str(exc))\n \n loop.run_forever()\ndiff --git a/src/snekssh/app4.py b/src/snekssh/app4.py\nindex e54480f..187722c 100644\n--- a/src/snekssh/app4.py\n+++ b/src/snekssh/app4.py\n@@ -24,32 +24,39 @@\n \n-import asyncio, asyncssh, bcrypt, sys\n+import asyncio\n+import sys\n from typing import Optional\n \n- 'user': bcrypt.hashpw(b'user', bcrypt.gensalt()),\n- }\n+import asyncssh\n+import bcrypt\n+\n+passwords = {\n+ \"user\": bcrypt.hashpw(b\"user\", bcrypt.gensalt()),\n+}\n+\n \n def handle_client(process: asyncssh.SSHServerProcess) -> None:\n- username = process.get_extra_info('username')\n- process.stdout.write(f'Welcome to my SSH server, {username}!\\n')\n+ username = process.get_extra_info(\"username\")\n+ process.stdout.write(f\"Welcome to my SSH server, {username}!\\n\")\n+\n \n class MySSHServer(asyncssh.SSHServer):\n def connection_made(self, conn: asyncssh.SSHServerConnection) -> None:\n- peername = conn.get_extra_info('peername')[0]\n- print(f'SSH connection received from {peername}.')\n+ peername = conn.get_extra_info(\"peername\")[0]\n+ print(f\"SSH connection received from {peername}.\")\n \n def connection_lost(self, exc: Optional[Exception]) -> None:\n if exc:\n- print('SSH connection error: ' + str(exc), file=sys.stderr)\n+ print(\"SSH connection error: \" + str(exc), file=sys.stderr)\n else:\n- print('SSH connection closed.')\n+ print(\"SSH connection closed.\")\n \n def begin_auth(self, username: str) -> bool:\n- return passwords.get(username) != b''\n+ return passwords.get(username) != b\"\"\n \n def password_auth_supported(self) -> bool:\n return True\n@@ -60,18 +67,24 @@ class MySSHServer(asyncssh.SSHServer):\n pw = passwords[username]\n if not password and not pw:\n return True\n- return bcrypt.checkpw(password.encode('utf-8'), pw)\n+ return bcrypt.checkpw(password.encode(\"utf-8\"), pw)\n+\n \n async def start_server() -> None:\n- await asyncssh.create_server(MySSHServer, '', 2231,\n- server_host_keys=['ssh_host_key'],\n- process_factory=handle_client)\n+ await asyncssh.create_server(\n+ MySSHServer,\n+ \"\",\n+ 2231,\n+ server_host_keys=[\"ssh_host_key\"],\n+ process_factory=handle_client,\n+ )\n+\n \n loop = asyncio.new_event_loop()\n \n try:\n loop.run_until_complete(start_server())\n except (OSError, asyncssh.Error) as exc:\n- sys.exit('Error starting server: ' + str(exc))\n+ sys.exit(\"Error starting server: \" + str(exc))\n \n loop.run_forever()\ndiff --git a/src/snekssh/app5.py b/src/snekssh/app5.py\nindex bbaf3d3..cfd5d21 100644\n--- a/src/snekssh/app5.py\n+++ b/src/snekssh/app5.py\n@@ -27,11 +27,15 @@\n \n-import asyncio, asyncssh, sys\n+import asyncio\n+import sys\n from typing import List, cast\n \n+import asyncssh\n+\n+\n class ChatClient:\n- _clients: List['ChatClient'] = []\n+ _clients: List[\"ChatClient\"] = []\n \n def __init__(self, process: asyncssh.SSHServerProcess):\n self._process = process\n@@ -40,8 +44,6 @@ class ChatClient:\n async def handle_client(cls, process: asyncssh.SSHServerProcess):\n await cls(process).run()\n \n- \n-\n async def readline(self) -> str:\n return cast(str, self._process.stdin.readline())\n \n@@ -53,54 +55,58 @@ class ChatClient:\n if client != self:\n client.write(msg)\n \n-\n def begin_auth(self, username: str) -> bool:\n \n def password_auth_supported(self) -> bool:\n return True\n \n def validate_password(self, username: str, password: str) -> bool:\n return True\n-\n \n async def run(self) -> None:\n- self.write('Welcome to chat!\\n\\n')\n+ self.write(\"Welcome to chat!\\n\\n\")\n \n- self.write('Enter your name: ')\n- name = (await self.readline()).rstrip('\\n')\n+ self.write(\"Enter your name: \")\n+ name = (await self.readline()).rstrip(\"\\n\")\n \n- self.write(f'\\n{len(self._clients)} other users are connected.\\n\\n')\n+ self.write(f\"\\n{len(self._clients)} other users are connected.\\n\\n\")\n \n self._clients.append(self)\n- self.broadcast(f'*** {name} has entered chat ***\\n')\n+ self.broadcast(f\"*** {name} has entered chat ***\\n\")\n \n try:\n async for line in self._process.stdin:\n- self.broadcast(f'{name}: {line}')\n+ self.broadcast(f\"{name}: {line}\")\n except asyncssh.BreakReceived:\n pass\n \n- self.broadcast(f'*** {name} has left chat ***\\n')\n+ self.broadcast(f\"*** {name} has left chat ***\\n\")\n self._clients.remove(self)\n \n+\n async def start_server() -> None:\n- await asyncssh.listen('', 2235, server_host_keys=['ssh_host_key'],\n- process_factory=ChatClient.handle_client)\n+ await asyncssh.listen(\n+ \"\",\n+ 2235,\n+ server_host_keys=[\"ssh_host_key\"],\n+ process_factory=ChatClient.handle_client,\n+ )\n+\n \n loop = asyncio.new_event_loop()\n \n try:\n loop.run_until_complete(start_server())\n except (OSError, asyncssh.Error) as exc:\n- sys.exit('Error starting server: ' + str(exc))\n+ sys.exit(\"Error starting server: \" + str(exc))\n \n loop.run_forever()"} +{"repo": ".", "date": "2025-03-29", "line": "feat: Initial implementation of WebDAV functionality with basic operations", "commit": "9a923f6bddd73df27af80ef6c8e2313816a07a48", "diff": "commit 9a923f6bddd73df27af80ef6c8e2313816a07a48\nAuthor: retoor \nDate: Sat Mar 29 07:15:53 2025 +0100\n\n WEebdav.\n\ndiff --git a/src/snek/__init__.py b/src/snek/__init__.py\nnew file mode 100644\nindex 0000000..b28b04f\n--- /dev/null\n+++ b/src/snek/__init__.py\n@@ -0,0 +1,3 @@\n+\n+\n+\ndiff --git a/src/snek/__main__.py b/src/snek/__main__.py\nnew file mode 100644\nindex 0000000..30f0209\n--- /dev/null\n+++ b/src/snek/__main__.py\n@@ -0,0 +1,5 @@\n+from aiohttp import web \n+from snek.app import Application\n+\n+if __name__ == '__main__':\n+ web.run_app(Application(), port=8081,host='0.0.0.0')\ndiff --git a/src/snek/webdav.py b/src/snek/webdav.py\nnew file mode 100755\nindex 0000000..66aeb3d\n--- /dev/null\n+++ b/src/snek/webdav.py\n@@ -0,0 +1,348 @@\n+import logging\n+\n+import pathlib\n+logging.basicConfig(level=logging.DEBUG)\n+\n+import asyncio\n+import base64\n+import datetime\n+import mimetypes\n+import os\n+import shutil\n+import uuid\n+\n+import aiofiles\n+import aiohttp\n+import aiohttp.web\n+from lxml import etree\n+\n+\n+class WebdavApplication(aiohttp.web.Application):\n+ def __init__(self, parent, *args, **kwargs):\n+ super().__init__(*args, **kwargs)\n+ self.locks = {}\n+ \n+ self.router.add_route(\"OPTIONS\", \"/{filename:.*}\", self.handle_options)\n+ self.router.add_route(\"GET\", \"/{filename:.*}\", self.handle_get)\n+ self.router.add_route(\"PUT\", \"/{filename:.*}\", self.handle_put)\n+ self.router.add_route(\"DELETE\", \"/{filename:.*}\", self.handle_delete)\n+ self.router.add_route(\"MKCOL\", \"/{filename:.*}\", self.handle_mkcol)\n+ self.router.add_route(\"MOVE\", \"/{filename:.*}\", self.handle_move)\n+ self.router.add_route(\"COPY\", \"/{filename:.*}\", self.handle_copy)\n+ self.router.add_route(\"PROPFIND\", \"/{filename:.*}\", self.handle_propfind)\n+ self.router.add_route(\"PROPPATCH\", \"/{filename:.*}\", self.handle_proppatch)\n+ self.router.add_route(\"LOCK\", \"/{filename:.*}\", self.handle_lock)\n+ self.router.add_route(\"UNLOCK\", \"/{filename:.*}\", self.handle_unlock)\n+ self.parent = parent\n+\n+ @property \n+ def db(self):\n+ return self.parent.db\n+\n+ @property \n+ def services(self):\n+ return self.parent.services \n+\n+\n+ async def authenticate(self, request):\n+ session = request.session\n+ if session.get('uid'):\n+ request['user'] = await self.services.user.get(uid=session['uid'])\n+ try:\n+ request['home'] = await self.services.user.get_home_folder(user_uid=request['user']['uid'])\n+ except:\n+ pass\n+ return user\n+\n+ auth_header = request.headers.get(\"Authorization\", \"\")\n+ if not auth_header.startswith(\"Basic \"):\n+ return False\n+ encoded_creds = auth_header.split(\"Basic \")[1]\n+ decoded_creds = base64.b64decode(encoded_creds).decode()\n+ username, password = decoded_creds.split(\":\", 1)\n+ request['user'] = await self.services.user.authenticate(username=username, password=password)\n+ try:\n+ request['home'] = await self.services.user.get_home_folder(request['user']['uid'])\n+ except Exception as ex:\n+ print(ex)\n+ pass\n+ return request['user']\n+\n+ async def handle_get(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+\n+ requested_path = request.match_info.get(\"filename\", \"\")\n+ abs_path = request['home'] / requested_path\n+\n+ if not abs_path.exists():\n+ return aiohttp.web.Response(status=404, text=\"File not found\")\n+\n+ if abs_path.is_dir():\n+ return aiohttp.web.Response(status=403, text=\"Cannot download a directory\")\n+\n+ content_type, _ = mimetypes.guess_type(str(abs_path))\n+ content_type = content_type or \"application/octet-stream\"\n+\n+ return aiohttp.web.FileResponse(\n+ path=str(abs_path), headers={\"Content-Type\": content_type}, chunk_size=8192\n+ )\n+\n+ async def handle_put(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ file_path = request['home'] / request.match_info[\"filename\"]\n+ file_path.parent.mkdir(parents=True, exist_ok=True)\n+ async with aiofiles.open(file_path, \"wb\") as f:\n+ while chunk := await request.content.read(1024):\n+ await f.write(chunk)\n+ return aiohttp.web.Response(status=201, text=\"File uploaded\")\n+\n+ async def handle_delete(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ file_path = request['home'] / request.match_info[\"filename\"]\n+ if file_path.is_file():\n+ file_path.unlink()\n+ return aiohttp.web.Response(status=204)\n+ elif file_path.is_dir():\n+ shutil.rmtree(file_path)\n+ return aiohttp.web.Response(status=204)\n+ return aiohttp.web.Response(status=404, text=\"Not found\")\n+\n+ async def handle_mkcol(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ dir_path = request['home'] / request.match_info[\"filename\"]\n+ if dir_path.exists():\n+ return aiohttp.web.Response(status=405, text=\"Directory already exists\")\n+ dir_path.mkdir(parents=True, exist_ok=True)\n+ return aiohttp.web.Response(status=201, text=\"Directory created\")\n+\n+ async def handle_move(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ src_path = request['home'] / request.match_info[\"filename\"]\n+ dest_path = request['home'] / request.headers.get(\"Destination\", \"\").replace(\n+ )\n+ if not src_path.exists():\n+ return aiohttp.web.Response(status=404, text=\"Source not found\")\n+ shutil.move(str(src_path), str(dest_path))\n+ return aiohttp.web.Response(status=201, text=\"Moved successfully\")\n+\n+ async def handle_copy(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ src_path = request['home'] / request.match_info[\"filename\"]\n+ dest_path = request['home'] / request.headers.get(\"Destination\", \"\").replace(\n+ )\n+ if not src_path.exists():\n+ return aiohttp.web.Response(status=404, text=\"Source not found\")\n+ if src_path.is_file():\n+ shutil.copy2(str(src_path), str(dest_path))\n+ else:\n+ shutil.copytree(str(src_path), str(dest_path))\n+ return aiohttp.web.Response(status=201, text=\"Copied successfully\")\n+\n+ async def handle_options(self, request):\n+ headers = {\n+ \"DAV\": \"1, 2\",\n+ \"Allow\": \"OPTIONS, GET, PUT, DELETE, MKCOL, MOVE, COPY, PROPFIND, PROPPATCH\",\n+ }\n+ print(\"RETURN\")\n+ return aiohttp.web.Response(status=200, headers=headers)\n+\n+ def get_current_utc_time(self, filepath):\n+ if filepath.exists():\n+ modified_time = datetime.datetime.utcfromtimestamp(filepath.stat().st_mtime)\n+ else:\n+ modified_time = datetime.datetime.utcnow()\n+ return modified_time.strftime(\"%Y-%m-%dT%H:%M:%SZ\"), modified_time.strftime(\n+ \"%a, %d %b %Y %H:%M:%S GMT\"\n+ )\n+\n+ def get_directory_size(self, directory):\n+ total_size = 0\n+ for dirpath, _, filenames in os.walk(directory):\n+ for f in filenames:\n+ fp = pathlib.Path(dirpath) / f\n+ if fp.exists():\n+ total_size += fp.stat().st_size\n+ return total_size\n+\n+ def get_disk_free_space(self, path):\n+ statvfs = os.statvfs(path)\n+ return statvfs.f_bavail * statvfs.f_frsize\n+\n+ async def handle_propfind(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ requested_path = request.match_info.get(\"filename\", \"\") \n+ abs_path = request['home'] / requested_path\n+ if not abs_path.exists():\n+ return aiohttp.web.Response(status=404, text=\"Directory not found\")\n+ nsmap = {\"D\": \"DAV:\"}\n+ response_xml = etree.Element(\"{DAV:}multistatus\", nsmap=nsmap)\n+ directories = [requested_path]\n+ if abs_path.is_dir():\n+ directories.extend(os.listdir(abs_path))\n+ for item in directories:\n+ full_path = abs_path / item if item != requested_path else abs_path\n+ href_path = f\"/{requested_path}/{item}/\" if item != requested_path else f\"/{requested_path}/\"\n+ response = etree.SubElement(response_xml, \"{DAV:}response\")\n+ href = etree.SubElement(response, \"{DAV:}href\")\n+ if not full_path.is_dir():\n+ href_path = href_path.rstrip(\"/\")\n+ href.text = href_path\n+ propstat = etree.SubElement(response, \"{DAV:}propstat\")\n+ prop = etree.SubElement(propstat, \"{DAV:}prop\")\n+ res_type = etree.SubElement(prop, \"{DAV:}resourcetype\")\n+ if full_path.is_dir():\n+ etree.SubElement(res_type, \"{DAV:}collection\")\n+ creation_date, last_modified = self.get_current_utc_time(full_path)\n+ etree.SubElement(prop, \"{DAV:}creationdate\").text = creation_date\n+ etree.SubElement(prop, \"{DAV:}quota-used-bytes\").text = str(\n+ full_path.stat().st_size\n+ if full_path.is_file()\n+ else self.get_directory_size(full_path)\n+ )\n+ etree.SubElement(prop, \"{DAV:}quota-available-bytes\").text = str(\n+ self.get_disk_free_space(request['home'])\n+ )\n+ etree.SubElement(prop, \"{DAV:}getlastmodified\").text = last_modified\n+ etree.SubElement(prop, \"{DAV:}displayname\").text = full_path.name\n+ etree.SubElement(prop, \"{DAV:}lockdiscovery\")\n+ mimetype, _ = mimetypes.guess_type(full_path.name)\n+ etree.SubElement(prop, \"{DAV:}contenttype\").text = mimetype\n+ etree.SubElement(prop, \"{DAV:}getcontentlength\").text = str(\n+ full_path.stat().st_size\n+ if full_path.is_file()\n+ else self.get_directory_size(full_path)\n+ )\n+ supported_lock = etree.SubElement(prop, \"{DAV:}supportedlock\")\n+ lock_entry_1 = etree.SubElement(supported_lock, \"{DAV:}lockentry\")\n+ lock_scope_1 = etree.SubElement(lock_entry_1, \"{DAV:}lockscope\")\n+ etree.SubElement(lock_scope_1, \"{DAV:}exclusive\")\n+ lock_type_1 = etree.SubElement(lock_entry_1, \"{DAV:}locktype\")\n+ etree.SubElement(lock_type_1, \"{DAV:}write\")\n+ lock_entry_2 = etree.SubElement(supported_lock, \"{DAV:}lockentry\")\n+ lock_scope_2 = etree.SubElement(lock_entry_2, \"{DAV:}lockscope\")\n+ etree.SubElement(lock_scope_2, \"{DAV:}shared\")\n+ lock_type_2 = etree.SubElement(lock_entry_2, \"{DAV:}locktype\")\n+ etree.SubElement(lock_type_2, \"{DAV:}write\")\n+ etree.SubElement(propstat, \"{DAV:}status\").text = \"HTTP/1.1 200 OK\"\n+ xml_output = etree.tostring(\n+ response_xml, encoding=\"utf-8\", xml_declaration=True\n+ ).decode()\n+ return aiohttp.web.Response(\n+ status=207, text=xml_output, content_type=\"application/xml\"\n+ )\n+\n+ async def handle_proppatch(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ return aiohttp.web.Response(status=207, text=\"PROPPATCH OK (Not Implemented)\")\n+\n+ async def handle_lock(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ resource = request.match_info.get(\"filename\", \"/\")\n+ lock_id = str(uuid.uuid4())\n+ self.locks[resource] = lock_id\n+ xml_response = self.generate_lock_response(lock_id)\n+ headers = {\n+ \"Lock-Token\": f\"opaquelocktoken:{lock_id}\",\n+ \"Content-Type\": \"application/xml\",\n+ }\n+ return aiohttp.web.Response(text=xml_response, headers=headers, status=200)\n+\n+ async def handle_unlock(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ resource = request.match_info.get(\"filename\", \"/\")\n+ lock_token = request.headers.get(\"Lock-Token\", \"\").replace(\n+ \"opaquelocktoken:\", \"\"\n+ )\n+ if self.locks.get(resource) == lock_token:\n+ del self.locks[resource]\n+ return aiohttp.web.Response(status=204)\n+ return aiohttp.web.Response(status=400, text=\"Invalid Lock Token\")\n+\n+ def generate_lock_response(self, lock_id):\n+ nsmap = {\"D\": \"DAV:\"}\n+ root = etree.Element(\"{DAV:}prop\", nsmap=nsmap)\n+ lock_discovery = etree.SubElement(root, \"{DAV:}lockdiscovery\")\n+ active_lock = etree.SubElement(lock_discovery, \"{DAV:}activelock\")\n+ lock_type = etree.SubElement(active_lock, \"{DAV:}locktype\")\n+ etree.SubElement(lock_type, \"{DAV:}write\")\n+ lock_scope = etree.SubElement(active_lock, \"{DAV:}lockscope\")\n+ etree.SubElement(lock_scope, \"{DAV:}exclusive\")\n+ etree.SubElement(active_lock, \"{DAV:}depth\").text = \"Infinity\"\n+ owner = etree.SubElement(active_lock, \"{DAV:}owner\")\n+ etree.SubElement(owner, \"{DAV:}href\").text = lock_id\n+ etree.SubElement(active_lock, \"{DAV:}timeout\").text = \"Infinite\"\n+ lock_token = etree.SubElement(active_lock, \"{DAV:}locktoken\")\n+ etree.SubElement(lock_token, \"{DAV:}href\").text = f\"opaquelocktoken:{lock_id}\"\n+ return etree.tostring(root, pretty_print=True, encoding=\"utf-8\").decode()\n+\n+ def get_last_modified(self, path):\n+ if not path.exists():\n+ return None\n+ timestamp = path.stat().st_mtime\n+ dt = datetime.datetime.utcfromtimestamp(timestamp)\n+ return dt.strftime(\"%a, %d %b %Y %H:%M:%S GMT\")\n+\n+ async def handle_head(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+\n+ requested_path = request.match_info.get(\"filename\", \"\")\n+ print(requested_path) \n+ abs_path = request['home'] / requested_path\n+\n+ if not abs_path.exists():\n+ return aiohttp.web.Response(status=404, text=\"File not found\")\n+\n+ if abs_path.is_dir():\n+ return aiohttp.web.Response(\n+ status=403, text=\"Cannot get metadata for a directory\"\n+ )\n+\n+ content_type, _ = mimetypes.guess_type(str(abs_path))\n+ content_type = content_type or \"application/octet-stream\"\n+ file_size = abs_path.stat().st_size\n+\n+ headers = {\n+ \"Content-Type\": content_type,\n+ \"Content-Length\": str(file_size),\n+ \"Last-Modified\": self.get_last_modified(abs_path),\n+ }\n+\n+ return aiohttp.web.Response(status=200, headers=headers)\n+\n+"} +{"repo": ".", "date": "2025-03-29", "line": "feat: Implemented WebDAV authentication and home folder retrieval", "commit": "886d21999c716ee306318796f3f159ba085f9618", "diff": "commit 886d21999c716ee306318796f3f159ba085f9618\nAuthor: retoor \nDate: Sat Mar 29 07:19:08 2025 +0100\n\n Added webdav.\n\ndiff --git a/src/snek/webdav.py b/src/snek/webdav.py\nindex 66aeb3d..3033e3a 100755\n--- a/src/snek/webdav.py\n+++ b/src/snek/webdav.py\n@@ -45,14 +45,14 @@ class WebdavApplication(aiohttp.web.Application):\n \n \n async def authenticate(self, request):\n- session = request.session\n- if session.get('uid'):\n- request['user'] = await self.services.user.get(uid=session['uid'])\n- try:\n- request['home'] = await self.services.user.get_home_folder(user_uid=request['user']['uid'])\n- except:\n- pass\n- return user\n \n auth_header = request.headers.get(\"Authorization\", \"\")\n if not auth_header.startswith(\"Basic \"):"} +{"repo": ".", "date": "2025-03-29", "line": "feat: Use `/home` instead of `/drive` for user home folders", "commit": "6dca3a96e1cd23cc26d69aeee31ae45e14977bbd", "diff": "commit 6dca3a96e1cd23cc26d69aeee31ae45e14977bbd\nAuthor: retoor \nDate: Sat Mar 29 07:27:56 2025 +0100\n\n Added webdav.\n\ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nindex 6055403..70e8adf 100644\n--- a/src/snek/service/user.py\n+++ b/src/snek/service/user.py\n@@ -40,7 +40,7 @@ class UserService(BaseService):\n return model\n \n async def get_home_folder(self, user_uid):\n- folder = pathlib.Path(f\"./drive/{user_uid}\")\n+ folder = pathlib.Path(f\"./home/{user_uid}\")\n if not folder.exists():\n folder.mkdir(parents=True, exist_ok=True)\n return folder"} +{"repo": ".", "date": "2025-03-29", "line": "feat: Implement node creation and enhance propfind with resource details", "commit": "3926b2d837bef181546d826b41821cf90fc755a8", "diff": "commit 3926b2d837bef181546d826b41821cf90fc755a8\nAuthor: retoor \nDate: Sat Mar 29 10:50:40 2025 +0100\n\n Update storage.\n\ndiff --git a/src/snek/webdav.py b/src/snek/webdav.py\nindex 3033e3a..e03e675 100755\n--- a/src/snek/webdav.py\n+++ b/src/snek/webdav.py\n@@ -188,11 +188,74 @@ class WebdavApplication(aiohttp.web.Application):\n statvfs = os.statvfs(path)\n return statvfs.f_bavail * statvfs.f_frsize\n \n+\n+ async def create_node(self, request, response_xml, full_path, depth):\n+ requested_path = request.match_info.get(\"filename\", \"\") \n+ abs_path = pathlib.Path(full_path)\n+ relative_path = str(full_path.relative_to(request['home']))\n+\n+ href_path = f\"{relative_path}\".strip(\"/\")\n+ response = etree.SubElement(response_xml, \"{DAV:}response\")\n+ href = etree.SubElement(response, \"{DAV:}href\")\n+ href.text = href_path\n+ propstat = etree.SubElement(response, \"{DAV:}propstat\")\n+ prop = etree.SubElement(propstat, \"{DAV:}prop\")\n+ res_type = etree.SubElement(prop, \"{DAV:}resourcetype\")\n+ if full_path.is_dir():\n+ etree.SubElement(res_type, \"{DAV:}collection\")\n+ creation_date, last_modified = self.get_current_utc_time(full_path)\n+ etree.SubElement(prop, \"{DAV:}creationdate\").text = creation_date\n+ etree.SubElement(prop, \"{DAV:}quota-used-bytes\").text = str(\n+ full_path.stat().st_size\n+ if full_path.is_file()\n+ else self.get_directory_size(full_path)\n+ )\n+ etree.SubElement(prop, \"{DAV:}quota-available-bytes\").text = str(\n+ self.get_disk_free_space(request['home'])\n+ )\n+ etree.SubElement(prop, \"{DAV:}getlastmodified\").text = last_modified\n+ etree.SubElement(prop, \"{DAV:}displayname\").text = full_path.name\n+ etree.SubElement(prop, \"{DAV:}lockdiscovery\")\n+ mimetype, _ = mimetypes.guess_type(full_path.name)\n+ etree.SubElement(prop, \"{DAV:}contenttype\").text = mimetype\n+ etree.SubElement(prop, \"{DAV:}getcontentlength\").text = str(\n+ full_path.stat().st_size\n+ if full_path.is_file()\n+ else self.get_directory_size(full_path)\n+ )\n+ supported_lock = etree.SubElement(prop, \"{DAV:}supportedlock\")\n+ lock_entry_1 = etree.SubElement(supported_lock, \"{DAV:}lockentry\")\n+ lock_scope_1 = etree.SubElement(lock_entry_1, \"{DAV:}lockscope\")\n+ etree.SubElement(lock_scope_1, \"{DAV:}exclusive\")\n+ lock_type_1 = etree.SubElement(lock_entry_1, \"{DAV:}locktype\")\n+ etree.SubElement(lock_type_1, \"{DAV:}write\")\n+ lock_entry_2 = etree.SubElement(supported_lock, \"{DAV:}lockentry\")\n+ lock_scope_2 = etree.SubElement(lock_entry_2, \"{DAV:}lockscope\")\n+ etree.SubElement(lock_scope_2, \"{DAV:}shared\")\n+ lock_type_2 = etree.SubElement(lock_entry_2, \"{DAV:}locktype\")\n+ etree.SubElement(lock_type_2, \"{DAV:}write\")\n+ etree.SubElement(propstat, \"{DAV:}status\").text = \"HTTP/1.1 200 OK\"\n+ \n+ if abs_path.is_dir() and depth != -1:\n+ for item in abs_path.iterdir():\n+ await self.create_node(request,response_xml, item, depth - 1)\n+\n+\n+\n+\n async def handle_propfind(self, request):\n if not await self.authenticate(request):\n return aiohttp.web.Response(\n status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n )\n+\n+ depth = 0\n+ try:\n+ depth = int(request.headers.get(\"Depth\", \"0\"))\n+ except ValueError:\n+ pass\n requested_path = request.match_info.get(\"filename\", \"\") \n abs_path = request['home'] / requested_path\n if not abs_path.exists():\n@@ -200,54 +263,9 @@ class WebdavApplication(aiohttp.web.Application):\n nsmap = {\"D\": \"DAV:\"}\n response_xml = etree.Element(\"{DAV:}multistatus\", nsmap=nsmap)\n directories = [requested_path]\n- if abs_path.is_dir():\n- directories.extend(os.listdir(abs_path))\n- for item in directories:\n- full_path = abs_path / item if item != requested_path else abs_path\n- href_path = f\"/{requested_path}/{item}/\" if item != requested_path else f\"/{requested_path}/\"\n- response = etree.SubElement(response_xml, \"{DAV:}response\")\n- href = etree.SubElement(response, \"{DAV:}href\")\n- if not full_path.is_dir():\n- href_path = href_path.rstrip(\"/\")\n- href.text = href_path\n- propstat = etree.SubElement(response, \"{DAV:}propstat\")\n- prop = etree.SubElement(propstat, \"{DAV:}prop\")\n- res_type = etree.SubElement(prop, \"{DAV:}resourcetype\")\n- if full_path.is_dir():\n- etree.SubElement(res_type, \"{DAV:}collection\")\n- creation_date, last_modified = self.get_current_utc_time(full_path)\n- etree.SubElement(prop, \"{DAV:}creationdate\").text = creation_date\n- etree.SubElement(prop, \"{DAV:}quota-used-bytes\").text = str(\n- full_path.stat().st_size\n- if full_path.is_file()\n- else self.get_directory_size(full_path)\n- )\n- etree.SubElement(prop, \"{DAV:}quota-available-bytes\").text = str(\n- self.get_disk_free_space(request['home'])\n- )\n- etree.SubElement(prop, \"{DAV:}getlastmodified\").text = last_modified\n- etree.SubElement(prop, \"{DAV:}displayname\").text = full_path.name\n- etree.SubElement(prop, \"{DAV:}lockdiscovery\")\n- mimetype, _ = mimetypes.guess_type(full_path.name)\n- etree.SubElement(prop, \"{DAV:}contenttype\").text = mimetype\n- etree.SubElement(prop, \"{DAV:}getcontentlength\").text = str(\n- full_path.stat().st_size\n- if full_path.is_file()\n- else self.get_directory_size(full_path)\n- )\n- supported_lock = etree.SubElement(prop, \"{DAV:}supportedlock\")\n- lock_entry_1 = etree.SubElement(supported_lock, \"{DAV:}lockentry\")\n- lock_scope_1 = etree.SubElement(lock_entry_1, \"{DAV:}lockscope\")\n- etree.SubElement(lock_scope_1, \"{DAV:}exclusive\")\n- lock_type_1 = etree.SubElement(lock_entry_1, \"{DAV:}locktype\")\n- etree.SubElement(lock_type_1, \"{DAV:}write\")\n- lock_entry_2 = etree.SubElement(supported_lock, \"{DAV:}lockentry\")\n- lock_scope_2 = etree.SubElement(lock_entry_2, \"{DAV:}lockscope\")\n- etree.SubElement(lock_scope_2, \"{DAV:}shared\")\n- lock_type_2 = etree.SubElement(lock_entry_2, \"{DAV:}locktype\")\n- etree.SubElement(lock_type_2, \"{DAV:}write\")\n- etree.SubElement(propstat, \"{DAV:}status\").text = \"HTTP/1.1 200 OK\"\n+ \n+ await self.create_node(request, response_xml, abs_path, depth)\n+\n xml_output = etree.tostring(\n response_xml, encoding=\"utf-8\", xml_declaration=True\n ).decode()"} +{"repo": ".", "date": "2025-03-30", "line": "fix: Updated home folder path to drive and removed lock management", "commit": "d5917b94540aee206935354f438a6a7f893278ec", "diff": "commit d5917b94540aee206935354f438a6a7f893278ec\nAuthor: retoor \nDate: Sun Mar 30 06:54:25 2025 +0200\n\n New version.\n\ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nindex 70e8adf..6055403 100644\n--- a/src/snek/service/user.py\n+++ b/src/snek/service/user.py\n@@ -40,7 +40,7 @@ class UserService(BaseService):\n return model\n \n async def get_home_folder(self, user_uid):\n- folder = pathlib.Path(f\"./home/{user_uid}\")\n+ folder = pathlib.Path(f\"./drive/{user_uid}\")\n if not folder.exists():\n folder.mkdir(parents=True, exist_ok=True)\n return folder\ndiff --git a/src/snek/webdav.py b/src/snek/webdav.py\nindex e03e675..b823c19 100755\n--- a/src/snek/webdav.py\n+++ b/src/snek/webdav.py\n@@ -287,7 +287,7 @@ class WebdavApplication(aiohttp.web.Application):\n )\n resource = request.match_info.get(\"filename\", \"/\")\n lock_id = str(uuid.uuid4())\n- self.locks[resource] = lock_id\n xml_response = self.generate_lock_response(lock_id)\n headers = {\n \"Lock-Token\": f\"opaquelocktoken:{lock_id}\","} +{"repo": ".", "date": "2025-03-30", "line": "feat: Add missing LOCK and UNLOCK routes", "commit": "8058e4a4b0aee254edc76fa28b2c4336eb393c4b", "diff": "commit 8058e4a4b0aee254edc76fa28b2c4336eb393c4b\nAuthor: retoor \nDate: Sun Mar 30 07:02:35 2025 +0200\n\n New version.\n\ndiff --git a/src/snek/webdav.py b/src/snek/webdav.py\nindex b823c19..a372b8b 100755\n--- a/src/snek/webdav.py\n+++ b/src/snek/webdav.py\n@@ -31,8 +31,8 @@ class WebdavApplication(aiohttp.web.Application):\n self.router.add_route(\"COPY\", \"/{filename:.*}\", self.handle_copy)\n self.router.add_route(\"PROPFIND\", \"/{filename:.*}\", self.handle_propfind)\n self.router.add_route(\"PROPPATCH\", \"/{filename:.*}\", self.handle_proppatch)\n- self.router.add_route(\"LOCK\", \"/{filename:.*}\", self.handle_lock)\n- self.router.add_route(\"UNLOCK\", \"/{filename:.*}\", self.handle_unlock)\n self.parent = parent\n \n @property"} +{"repo": ".", "date": "2025-03-30", "line": "fix: Handle potential errors when creating home folders", "commit": "2a47c0ba5e7344d44c3acd15d8f3efeb11722677", "diff": "commit 2a47c0ba5e7344d44c3acd15d8f3efeb11722677\nAuthor: retoor \nDate: Sun Mar 30 09:34:13 2025 +0200\n\n Fixed bug.\n\ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nindex 6055403..b70be63 100644\n--- a/src/snek/service/user.py\n+++ b/src/snek/service/user.py\n@@ -42,7 +42,10 @@ class UserService(BaseService):\n async def get_home_folder(self, user_uid):\n folder = pathlib.Path(f\"./drive/{user_uid}\")\n if not folder.exists():\n- folder.mkdir(parents=True, exist_ok=True)\n+ try:\n+ folder.mkdir(parents=True, exist_ok=True)\n+ except:\n+ pass\n return folder\n \n async def register(self, email, username, password):"} +{"repo": ".", "date": "2025-04-01", "line": "feat: Added settings page with profile, notifications, and privacy sections", "commit": "32e0c959e8589f574d1c46b96d35f49c98721566", "diff": "commit 32e0c959e8589f574d1c46b96d35f49c98721566\nAuthor: retoor \nDate: Tue Apr 1 16:21:29 2025 +0200\n\n Progress.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 984fcf3..d043c0e 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -42,6 +42,7 @@ from snek.view.terminal import TerminalSocketView, TerminalView\n from snek.view.upload import UploadView\n from snek.view.web import WebView\n from snek.webdav import WebdavApplication\n+from snek.view.settings import SettingsView\n \n SESSION_KEY = b\"c79a0c5fda4b424189c427d28c9f7c34\"\n \n@@ -137,6 +138,7 @@ class Application(BaseApplication):\n self.router.add_view(\"/docs.html\", DocsHTMLView)\n self.router.add_view(\"/docs.md\", DocsMDView)\n self.router.add_view(\"/status.json\", StatusView)\n+ self.router.add_view(\"/settings.html\", SettingsView)\n self.router.add_view(\"/web.html\", WebView)\n self.router.add_view(\"/login.html\", LoginView)\n self.router.add_view(\"/login.json\", LoginView)\ndiff --git a/src/snek/templates/settings.html b/src/snek/templates/settings.html\nnew file mode 100644\nindex 0000000..cfb186c\n--- /dev/null\n+++ b/src/snek/templates/settings.html\n@@ -0,0 +1,36 @@\n+{% extends \"app.html\" %}\n+\n+{% block sidebar %}\n+\n+{% include \"sidebar_settings.html\" %}\n+\n+{% endblock %}\n+\n+{% block head %}\n+\n+\n+{% endblock %}\n+\n+{% block main %}\n+\n+

Setting page

\n+\n+
\n+\n+\n+\n+\n+\n+{% endblock main %}\ndiff --git a/src/snek/templates/sidebar_settings.html b/src/snek/templates/sidebar_settings.html\nnew file mode 100644\nindex 0000000..8e18412\n--- /dev/null\n+++ b/src/snek/templates/sidebar_settings.html\n@@ -0,0 +1,14 @@\n+\n+\ndiff --git a/src/snek/view/settings.py b/src/snek/view/settings.py\nnew file mode 100644\nindex 0000000..fe181f2\n--- /dev/null\n+++ b/src/snek/view/settings.py\n@@ -0,0 +1,8 @@\n+from snek.system.view import BaseView \n+\n+class SettingsView(BaseView):\n+ \n+ login_required = True\n+\n+ async def get(self):\n+ return await self.render_template('settings.html')\ndiff --git a/src/snek/webdav.py b/src/snek/webdav.py\nindex a372b8b..9a2b9e4 100755\n--- a/src/snek/webdav.py\n+++ b/src/snek/webdav.py\n@@ -219,8 +219,9 @@ class WebdavApplication(aiohttp.web.Application):\n etree.SubElement(prop, \"{DAV:}displayname\").text = full_path.name\n etree.SubElement(prop, \"{DAV:}lockdiscovery\")\n mimetype, _ = mimetypes.guess_type(full_path.name)\n- etree.SubElement(prop, \"{DAV:}contenttype\").text = mimetype\n- etree.SubElement(prop, \"{DAV:}getcontentlength\").text = str(\n+ if full_path.is_file():\n+ etree.SubElement(prop, \"{DAV:}contenttype\").text = mimetype\n+ etree.SubElement(prop, \"{DAV:}getcontentlength\").text = str(\n full_path.stat().st_size\n if full_path.is_file()\n else self.get_directory_size(full_path)"} +{"repo": ".", "date": "2025-04-01", "line": "feat: Improve responsiveness for smaller screens and reposition chat input", "commit": "87b48af551d2ed023f77ca57d033ed0079d303f3", "diff": "commit 87b48af551d2ed023f77ca57d033ed0079d303f3\nAuthor: retoor \nDate: Tue Apr 1 20:08:01 2025 +0200\n\n Update css.\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex 31a6e74..db9dcb0 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -347,3 +347,15 @@ a {\n }\n \n+@media only screen and (max-width: 600px) {\n+ header{\n+ position: sticky;\n+ display: block;\n+ .logo {\n+ display:block;\n+ }\n+ }\n+ .chat-input {\n+ position:sticky;\n+ }\n+}\ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex 28ad4d2..d386b67 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -12,10 +12,10 @@\n {% endfor %}\n
\n \n-
\n+
\n \n \n-
\n+ \n
\n \n \n \n@@ -14,9 +21,11 @@\n

Snek

\n

Rocket Chat got bloated, too commercialized,\n So Snek came through, lean and optimized.

\n+
\n \n OR\n \n+
\n
\n \n \ndiff --git a/src/snek/view/login.py b/src/snek/view/login.py\nindex be79328..fe8cf4d 100644\n--- a/src/snek/view/login.py\n+++ b/src/snek/view/login.py\n@@ -15,6 +15,8 @@ from snek.system.view import BaseFormView\n class LoginView(BaseFormView):\n form = LoginForm\n \n+ login_required = False\n+\n async def get(self):\n if self.session.get(\"logged_in\"):\n return web.HTTPFound(\"/web.html\")\ndiff --git a/src/snek/view/register.py b/src/snek/view/register.py\nindex 7fbce9d..96eed8a 100644\n--- a/src/snek/view/register.py\n+++ b/src/snek/view/register.py\n@@ -15,6 +15,8 @@ from snek.system.view import BaseFormView\n class RegisterView(BaseFormView):\n form = RegisterForm\n \n+ login_required = False\n+\n async def get(self):\n if self.session.get(\"logged_in\"):\n return web.HTTPFound(\"/web.html\")"} +{"repo": ".", "date": "2025-04-01", "line": "feat: Reduced container width for mobile responsiveness", "commit": "e3c997302b07228c0791d6307b429109b6eb3d53", "diff": "commit e3c997302b07228c0791d6307b429109b6eb3d53\nAuthor: retoor \nDate: Tue Apr 1 21:09:58 2025 +0200\n\n Upated for phone.\n\ndiff --git a/src/snek/static/style.css b/src/snek/static/style.css\nindex f10f780..6e81345 100644\n--- a/src/snek/static/style.css\n+++ b/src/snek/static/style.css\n@@ -13,7 +13,7 @@\n \n border-radius: 10px;\n padding: 30px;\n- width: 600px;\n+ width: 500px;\n margin: 30px;\n box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n }"} +{"repo": ".", "date": "2025-04-01", "line": "feat: Adjusted form width for better mobile responsiveness", "commit": "27c0abea3147e5e00a5196fa9b351141a9d17ae2", "diff": "commit 27c0abea3147e5e00a5196fa9b351141a9d17ae2\nAuthor: retoor \nDate: Tue Apr 1 21:23:10 2025 +0200\n\n Upated for phone.\n\ndiff --git a/src/snek/static/generic-form.css b/src/snek/static/generic-form.css\nindex d593816..7010193 100644\n--- a/src/snek/static/generic-form.css\n+++ b/src/snek/static/generic-form.css\n@@ -14,6 +14,7 @@\n justify-content: center;\n align-items: center;\n height: 100vh;\n+ width: 100vw;\n }\n \n generic-form {\n@@ -29,7 +30,6 @@ generic-form {\n border-radius: 10px;\n padding: 30px;\n- width: 400px;\n box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n text-align: center;\n \n@@ -93,8 +93,7 @@ input {\n }\n \n \n-@media (max-width: 500px) {\n+@media (max-width: 600px) {\n .generic-form-container {\n- width: 90%;\n }\n-}\n\\ No newline at end of file\n+}"} +{"repo": ".", "date": "2025-04-02", "line": "fix: Handle missing last message gracefully", "commit": "b365afc910522a484ad257af6df7b607712c71f2", "diff": "commit b365afc910522a484ad257af6df7b607712c71f2\nAuthor: retoor \nDate: Wed Apr 2 10:52:02 2025 +0200\n\n Update.\n\ndiff --git a/src/snek/model/channel.py b/src/snek/model/channel.py\nindex 183ddb0..0a90c39 100644\n--- a/src/snek/model/channel.py\n+++ b/src/snek/model/channel.py\n@@ -13,12 +13,15 @@ class ChannelModel(BaseModel):\n last_message_on = ModelField(name=\"last_message_on\", required=False, kind=str)\n \n async def get_last_message(self) -> ChannelMessageModel:\n- async for model in self.app.services.channel_message.query(\n- \"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1\",\n- {\"channel_uid\": self[\"uid\"]},\n- ):\n+ try:\n+ async for model in self.app.services.channel_message.query(\n+ \"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1\",\n+ {\"channel_uid\": self[\"uid\"]},\n+ ):\n \n- return await self.app.services.channel_message.get(uid=model[\"uid\"])\n+ return await self.app.services.channel_message.get(uid=model[\"uid\"])\n+ except:\n+ pass\n return None\n \n async def get_members(self):"} +{"repo": ".", "date": "2025-04-02", "line": "feat: Add UUID to context and style updates for smaller screens", "commit": "99b2beeab0d55242537b7ec810bfdd69feb47103", "diff": "commit 99b2beeab0d55242537b7ec810bfdd69feb47103\nAuthor: retoor \nDate: Wed Apr 2 14:55:18 2025 +0200\n\n Updates.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex d043c0e..c6c2e2f 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -2,7 +2,7 @@ import asyncio\n import logging\n import pathlib\n import time\n-\n+import uuid\n from snek.view.threads import ThreadsView\n \n logging.basicConfig(level=logging.DEBUG)\n@@ -189,6 +189,8 @@ class Application(BaseApplication):\n channels = []\n if not context:\n context = {}\n+ \n+ context['rid'] = str(uuid.uuid4())\n if request.session.get(\"uid\"):\n async for subscribed_channel in self.services.channel_member.find(\n user_uid=request.session.get(\"uid\"), deleted_at=None, is_banned=False\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex 7a4617d..049241d 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -42,6 +42,7 @@ header {\n padding-top: 10px;\n padding-left: 20px;\n padding-right: 20px;\n+ padding-bottom: 10px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n@@ -79,6 +80,17 @@ a {\n margin-bottom: 3px;\n }\n \n+h1 {\n+ font-size: 2em;\n+}\n+\n+h2 {\n+ font-size: 1.4em;\n+}\n+\n+\n .chat-area {\n flex: 1;\n display: flex;\n@@ -354,7 +366,31 @@ a {\n }\n \n-@media only screen and (max-width: 600px) {\n+@media only screen and (max-width: 768px) {\n+ \n+ header{\n+ position:fixed;\n+ top: 0;\n+ left: 0;\n+ text-overflow: ellipsis;\n+\n+ *{\n+ font-size: 12px !important;\n+ }\n+ .logo {\n+ text-overflow: ellipsis;\n+ white-space: nowrap;\n+ overflow: hidden;\n+ h2 {\n+ font-size: 12px;\n+ }\n+ }\n+ \n+ }\n+ body {\n+ justify-content: flex-start;\n+ }\n header{\n position: sticky;\n display: block;\n@@ -364,5 +400,5 @@ a {\n }\n .chat-input {\n position:sticky;\n- }\n }\ndiff --git a/src/snek/static/generic-form.css b/src/snek/static/generic-form.css\nindex 7010193..025bfb3 100644\n--- a/src/snek/static/generic-form.css\n+++ b/src/snek/static/generic-form.css\n@@ -34,11 +34,11 @@ generic-form {\n text-align: center;\n \n }\n-\n .generic-form-container h1 {\n font-size: 2em;\n margin-bottom: 20px;\n+\n }\n input {\n \ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nindex be4b327..f2edacb 100644\n--- a/src/snek/static/generic-form.js\n+++ b/src/snek/static/generic-form.js\n@@ -266,9 +266,9 @@ class GenericForm extends HTMLElement {\n }\n \n div {\n+ min-width:350px;\n border-radius: 10px;\n padding: 30px;\n- width: 400px;\n box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n text-align: center;\n }\ndiff --git a/src/snek/static/style.css b/src/snek/static/style.css\nindex 6e81345..af04db4 100644\n--- a/src/snek/static/style.css\n+++ b/src/snek/static/style.css\n@@ -56,3 +56,9 @@ div {\n text-align: left;\n \n }\n+\n+@media screen and (max-width: 500px) {\n+ body {\n+\n+ justify-content: flex-start;\n+ }\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 1533153..6c3f137 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -21,11 +21,12 @@\n \n \n+\n+\n+\n
\n-
Snek
\n- \n-
{% block header_text %}{% endblock %}
\n-
\n+\n+
\n+\n+
\n+
\n+

File Sharing

\n+
    \n+
  • SFTP storage with your Snek credentials
  • \n+
  • WebDAV support \u2013 same login
  • \n+
\n+
\n+\n+
\n+

Git Repositories

\n+
    \n+
  • Configure repos for any official Git client
  • \n+
  • Instant setup, push & pull
  • \n+
\n+
\n+\n+
\n+

AI Powerhouse

\n+
    \n+
  • Chat with free & commercial AIs
  • \n+
  • Generate AI-powered images
  • \n+
  • Build your own AI bots in <5 minutes (copy/paste example)
  • \n+
\n+
\n+\n+
\n+

Dev & Terminal

\n+
    \n+
  • Ubuntu web terminal in-browser
  • \n+
  • Full profile & permissions management
  • \n+
\n+
\n+\n+
\n+

Chat & Media

\n+
    \n+
  • Upload any file type in chat
  • \n+
  • Rich media support (audio, video, images\u2026)
  • \n+
  • Direct messaging with other users
  • \n+
\n+
\n+\n+
\n+

Privacy & Community

\n+
    \n+
  • No logging\u2014never even your IP
  • \n+
  • No email required to sign up
  • \n+
  • Multi-national, open community
  • \n+
  • Hacking encouraged!
  • \n+
\n+
\n+\n+
\n+

Customization & Deployment

\n+
    \n+
  • Full layout & theme customization
  • \n+
  • Install as a PWA on your phone
  • \n+
  • Optionally self-host: pip install snek, zero config
  • \n+
\n+
\n+
\n+\n+
\n+

Ready to join?

\n+

No email. No logs. Just sign up, pick a username, and dive in!

\n+ Sign Up Now\n+
\n+\n+
\n+

Self-Host in Seconds

\n+

Just run:

\n+snek serve\n+ \n+

No configuration required\u2014it's that simple.

\n+
\n+\n+
\n+\n+
\n+

© 2025 Snek \u2013 Join our global community of developers, testers & AI enthusiasts.

\n+
\n+\n \n "} +{"repo": ".", "date": "2025-05-17", "line": "feat: Refactor chat input component with auto-completion and live typing support", "commit": "48c3daf3983e3b6e04a0c5888febceb69db9d661", "diff": "commit 48c3daf3983e3b6e04a0c5888febceb69db9d661\nAuthor: retoor \nDate: Sat May 17 00:54:15 2025 +0200\n\n Update.\n\ndiff --git a/src/snek/static/chat-input.js b/src/snek/static/chat-input.js\nindex c1d767d..2d0914e 100644\n--- a/src/snek/static/chat-input.js\n+++ b/src/snek/static/chat-input.js\n@@ -1,69 +1,234 @@\n-\n-\n-\n-\n-class ChatInputElement extends HTMLElement {\n- _chatWindow = null \n- constructor() {\n- super();\n- this.attachShadow({ mode: 'open' });\n- this.component = document.createElement('div');\n- this.shadowRoot.appendChild(this.component);\n- }\n- set chatWindow(value){\n- this._chatWindow = value \n-\n- }\n- get chatWindow(){\n- return this._chatWindow \n- }\n- get channelUid() {\n- return this.chatWindow.channel.uid\n- }\n- connectedCallback() {\n- const link = document.createElement('link');\n- link.rel = 'stylesheet';\n- link.href = '/base.css';\n- this.component.appendChild(link);\n-\n- this.container = document.createElement('div');\n- this.container.classList.add('chat-input');\n- this.container.innerHTML = `\n- \n- \n- `;\n- this.textBox = this.container.querySelector('textarea');\n- this.uploadButton = this.container.querySelector('upload-button');\n- this.uploadButton.chatInput = this \n- this.textBox.addEventListener('input', (e) => {\n- this.dispatchEvent(new CustomEvent('input', { detail: e.target.value, bubbles: true }));\n- const message = e.target.value;\n- const button = this.container.querySelector('button');\n- button.disabled = !message;\n- });\n-\n- this.textBox.addEventListener('change', (e) => {\n- e.preventDefault();\n- this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));\n- console.error(e.target.value);\n- });\n-\n- this.textBox.addEventListener('keydown', (e) => {\n- if (e.key === 'Enter' && !e.shiftKey) {\n- e.preventDefault();\n- const message = e.target.value.trim();\n- if (!message) return;\n- this.dispatchEvent(new CustomEvent('submit', { detail: message, bubbles: true }));\n- e.target.value = '';\n- }\n- });\n-\n- this.component.appendChild(this.container);\n- }\n+\n+import { app } from '../app.js';\n+\n+class ChatInputComponent extends HTMLElement {\n+ autoCompletions = {\n+ 'example 1': () => {\n+\n+ },\n+ 'example 2': () => {\n+\n+ }\n+ }\n+\n+ constructor() {\n+ super();\n+ this.lastUpdateEvent = new Date();\n+ this.textarea = document.createElement(\"textarea\");\n+ this._value = \"\";\n+ this.value = this.getAttribute(\"value\") || \"\";\n+ this.previousValue = this.value;\n+ this.lastChange = new Date();\n+ this.changed = false;\n+ }\n+\n+ get value() {\n+ return this._value;\n+ }\n+\n+ set value(value) {\n+ this._value = value || \"\";\n+ this.textarea.value = this._value;\n+ }\n+\n+ resolveAutoComplete() {\n+ let count = 0;\n+ let value = null;\n+ Object.keys(this.autoCompletions).forEach((key) => {\n+ if (key.startsWith(this.value)) {\n+ count++;\n+ value = key;\n+ }\n+ });\n+ if (count == 1)\n+ return value;\n+ return null;\n+ }\n+\n+ isActive() {\n+ return document.activeElement === this.textarea;\n+ }\n+\n+ focus() {\n+ this.textarea.focus();\n+ }\n+\n+ connectedCallback() {\n+ this.liveType = this.getAttribute(\"live-type\") === \"true\";\n+ this.liveTypeInterval = parseInt(this.getAttribute(\"live-type-interval\")) || 3;\n+ this.channelUid = this.getAttribute(\"channel\");\n+ this.messageUid = null;\n+\n+ this.classList.add(\"chat-input\");\n+\n+ this.textarea.setAttribute(\"placeholder\", \"Type a message...\");\n+ this.textarea.setAttribute(\"rows\", \"2\");\n+\n+ this.appendChild(this.textarea);\n+\n+ this.uploadButton = document.createElement(\"upload-button\");\n+ this.uploadButton.setAttribute(\"channel\", this.channelUid);\n+ this.uploadButton.addEventListener(\"upload\", (e) => {\n+ this.dispatchEvent(new CustomEvent(\"upload\", e));\n+ });\n+ this.uploadButton.addEventListener(\"uploaded\", (e) => {\n+ this.dispatchEvent(new CustomEvent(\"uploaded\", e));\n+ });\n+\n+ this.appendChild(this.uploadButton);\n+\n+ this.textarea.addEventListener(\"keyup\", (e) => {\n+ if(e.key === 'Enter' && !e.shiftKey) {\n+ this.value = ''\n+ e.target.value = '';\n+ return \n+ }\n+ this.value = e.target.value;\n+ this.changed = true;\n+ this.update();\n+ });\n+\n+ this.textarea.addEventListener(\"keydown\", (e) => {\n+ this.value = e.target.value;\n+ if (e.key === \"Tab\") {\n+ e.preventDefault();\n+ let autoCompletion = this.resolveAutoComplete();\n+ if (autoCompletion) {\n+ e.target.value = autoCompletion;\n+ this.value = autoCompletion;\n+ return;\n+ }\n+ }\n+ if (e.key === 'Enter' && !e.shiftKey) {\n+ e.preventDefault();\n+\n+ const message = e.target.value;\n+ this.messageUid = null;\n+ this.value = '';\n+ this.previousValue = '';\n+\n+ if (!message) {\n+ return;\n+ }\n+\n+ let autoCompletion = this.autoCompletions[message];\n+ if (autoCompletion) {\n+ this.value = '';\n+ this.previousValue = '';\n+ e.target.value = '';\n+ autoCompletion();\n+ return;\n+ }\n+\n+ e.target.value = '';\n+ this.value = '';\n+ this.messageUid = null;\n+ this.sendMessage(this.channelUid, message).then((uid) => {\n+ this.messageUid = uid;\n+ });\n+ }\n+ });\n+\n+ this.changeInterval = setInterval(() => {\n+ if (!this.liveType) {\n+ return;\n+ }\n+ if (this.value !== this.previousValue) {\n+ if (this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= this.liveTypeInterval) {\n+ this.value = '';\n+ this.previousValue = '';\n+ }\n+ this.lastChange = new Date();\n+ }\n+ this.update();\n+ }, 300);\n+\n+ this.addEventListener(\"upload\", (e) => {\n+ this.focus();\n+ });\n+ this.addEventListener(\"uploaded\", function (e) {\n+ let message = \"\";\n+ e.detail.files.forEach((file) => {\n+ message += `[${file.name}](/channel/attachment/${file.relative_url})`;\n+ });\n+ app.rpc.sendMessage(this.channelUid, message);\n+ });\n+ }\n+\n+ trackSecondsBetweenEvents(event1Time, event2Time) {\n+ const millisecondsDifference = event2Time.getTime() - event1Time.getTime();\n+ return millisecondsDifference / 1000;\n+ }\n+\n+ newMessage() {\n+ if (!this.messageUid) {\n+ this.messageUid = '?';\n+ }\n+\n+ this.sendMessage(this.channelUid, this.value).then((uid) => {\n+ this.messageUid = uid;\n+ });\n+ }\n+\n+ updateMessage() {\n+ if (this.value[0] == \"/\") {\n+ return false;\n+ }\n+ if (!this.messageUid) {\n+ this.newMessage();\n+ return false;\n+ }\n+ if (this.messageUid === '?') {\n+ return false;\n+ }\n+ if (typeof app !== \"undefined\" && app.rpc && typeof app.rpc.updateMessageText === \"function\") {\n+ app.rpc.updateMessageText(this.messageUid, this.value);\n+ }\n+ }\n+\n+ updateStatus() {\n+ if (this.liveType) {\n+ return;\n+ }\n+ if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) {\n+ this.lastUpdateEvent = new Date();\n+ if (typeof app !== \"undefined\" && app.rpc && typeof app.rpc.set_typing === \"function\") {\n+ app.rpc.set_typing(this.channelUid);\n+ }\n+ }\n+ }\n+\n+ update() {\n+ const expired = this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= this.liveTypeInterval;\n+ const changed = (this.value !== this.previousValue);\n+\n+ if (changed || expired) {\n+ this.lastChange = new Date();\n+ this.updateStatus();\n+ }\n+\n+ this.previousValue = this.value;\n+\n+ if (this.liveType && expired) {\n+ this.value = \"\";\n+ this.previousValue = \"\";\n+ this.messageUid = null;\n+ return;\n+ }\n+\n+ if (changed) {\n+ if (this.liveType) {\n+ this.updateMessage();\n+ }\n+ }\n+ }\n+\n+ async sendMessage(channelUid, value) {\n+ if (!value.trim()) {\n+ return null;\n+ }\n+ return await app.rpc.sendMessage(channelUid, value);\n+ }\n }\n \n-customElements.define('chat-input', ChatInputElement);\n+customElements.define('chat-input', ChatInputComponent);\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 7baa67a..596b5d1 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -18,6 +18,7 @@\n \n \n \n+ \n \n \n \ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex 02e60df..94d0ac5 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -4,10 +4,6 @@\n \n {% block main %}\n \n-\n-\n-\n-\n
\n \n {% for message in messages %}\n@@ -16,10 +12,7 @@\n {% endautoescape %}\n {% endfor %}\n \n-
\n- \n- \n-
\n+ \n
\n {% include \"dialog_help.html\" %}\n {% include \"dialog_online.html\" %}\n@@ -27,11 +20,8 @@\n import { app } from \"/app.js\";\n import { Schedule } from \"/schedule.js\";\n const channelUid = \"{{ channel.uid.value }}\";\n-\n- function getInputField(){\n- return document.querySelector(\"textarea\")\n- }\n- getInputField().autoComplete = {\n+ const chatInputField = document.querySelector(\"chat-input\");\n+ chatInputField.autoCompletions = {\n \"/online\": () =>{\n showOnline();\n },\n@@ -39,117 +29,15 @@\n document.querySelector(\".chat-messages\").innerHTML = '';\n },\n \"/live\": () =>{\n- getInputField().liveType = !getInputField().liveType\n+ \n+ chatInputField.liveType = !chatInputField.liveType\n },\n \"/help\": () => {\n showHelp();\n }\n- }\n-\n-\n- function initInputField(textBox) {\n- if(textBox.liveType == undefined){\n- textBox.liveType = false\n- }\n- let typeTimeout = null;\n- textBox.addEventListener('keydown',async (e) => {\n- if(typeTimeout){\n- clearTimeout(typeTimeout)\n- typeTimeout = null\n- }\n- if(e.target.liveType){\n- typeTimeout = setTimeout(()=>{\n- e.target.lastMessageUid = null\n- e.target.value = ''\n- },3000)\n- }\n- if(e.key === \"ArrowUp\"){\n- const value = findDivAboveText(e.target.value).querySelector('.text')\n- e.target.value = value.textContent\n- console.info(\"HIERR\")\n- return\n- }\n- if (e.key === \"Tab\") {\n-\n- const message = e.target.value.trim();\n- if (!message) {\n- return\n- }\n- let autoCompleteHandler = null;\n- Object.keys(e.target.autoComplete).forEach((key)=>{\n- if(key.startsWith(message)){\n- if(autoCompleteHandler){\n- return \n- }\n- autoCompleteHandler = key\n- }\n- })\n- if(autoCompleteHandler){\n- e.preventDefault();\n- e.target.value = autoCompleteHandler;\n- return\n- }\n- }\n- if (e.key === 'Enter' && !e.shiftKey) {\n- e.preventDefault();\n- const message = e.target.value.trim();\n- if (!message) {\n- return\n- }\n- let autoCompleteHandler = e.target.autoComplete[message]\n- if(autoCompleteHandler){\n- const value = message;\n- e.target.value = '';\n- autoCompleteHandler(value)\n- return\n- }\n-\n- e.target.value = '';\n- if(textBox.liveType && textBox.lastMessageUid && textBox.lastMessageUid != '?'){\n- \n- \n- app.rpc.updateMessageText(textBox.lastMessageUid, message)\n- textBox.lastMessageUid = null\n- return \n- }\n-\n- const messageResponse = await app.rpc.sendMessage(channelUid, message);\n- \n-\t }else{\n-\t\tif(textBox.liveType){\n- if(e.target.value.endsWith(\"\\n\") || e.target.value.endsWith(\" \")){\n- return\n- } \n- if(e.target.value[0] == \"/\"){\n- return\n- }\n- if(!textBox.lastMessageUid){\n- textBox.lastMessageUid = '?'\n- app.rpc.sendMessage(channelUid, e.target.value).then((messageResponse)=>{\n- textBox.lastMessageUid = messageResponse\n- })\n- }\n- if(textBox.lastMessageUid == '?'){\n- return;\n- }\n- app.rpc.updateMessageText(textBox.lastMessageUid, e.target.value)\n- }else{\n- app.rpc.set_typing(channelUid)\n- }\n-\n- \n-\t }\n- });\n- document.querySelector(\"upload-button\").addEventListener(\"upload\",function(e){\n- getInputField().focus();\n- })\n- document.querySelector(\"upload-button\").addEventListener(\"uploaded\",function(e){\n- let message = \"\"\n- e.detail.files.forEach((file)=>{\n- message += `[${file.name}](/channel/attachment/${file.relative_url})`\n- })\n- app.rpc.sendMessage(channelUid,message)\n- })\n+ }\n+ \n+ const textBox = document.querySelector(\"chat-input\").textarea\n textBox.addEventListener(\"paste\", async (e) => {\n try {\n const clipboardItems = await navigator.clipboard.read();\n@@ -168,7 +56,7 @@\n }\n \n if (dt.items.length > 0) {\n- const uploadButton = document.querySelector(\"upload-button\");\n+ const uploadButton = chatInputField.uploadButton\n const input = uploadButton.shadowRoot.querySelector('.file-input')\n input.files = dt.files;\n \n@@ -187,7 +75,7 @@\n \n const dt = e.dataTransfer;\n if (dt.items.length > 0) {\n- const uploadButton = document.querySelector(\"upload-button\");\n+ const uploadButton = chatInputField.uploadButton\n const input = uploadButton.shadowRoot.querySelector('.file-input')\n input.files = dt.files;\n \n@@ -197,13 +85,16 @@\n chatInput.addEventListener(\"dragover\", async (e) => {\n e.preventDefault();\n e.dataTransfer.dropEffect = \"link\";\n+ \n+\n })\n \n- textBox.focus();\n- }\n+ chatInputField.textarea.focus();\n+\n+ \n \n function replyMessage(message) {\n- const field = getInputField()\n+ const field = chatInputField\n field.value = \"```markdown\\n> \" + (message || '') + \"\\n```\\n\";\n field.focus();\n }\n@@ -294,8 +185,8 @@\n lastMessage = messagesContainer.querySelector(\".message:last-child\");\n if (doScrollDown) {\n lastMessage?.scrollIntoView({ block: \"end\", inline: \"nearest\" });\n- const inputBox = document.querySelector(\".chat-input\");\n- inputBox.scrollIntoView({ block: \"end\", inline: \"nearest\" });\n+ \n+ chatInputField.scrollIntoView({ block: \"end\", inline: \"nearest\" });\n }\n }\n \n@@ -378,17 +269,17 @@\n messagesContainer.querySelector(\".message:last-child\").scrollIntoView({ block: \"end\", inline: \"nearest\" });\n setTimeout(() => {\n \n- getInputField().focus();\n+ chatInputField.focus();\n },500)\n \n }\n }\n if (event.shiftKey && event.key === 'G') {\n- if(document.activeElement != getInputField()){\n+ if(chatInputField.isActive()){\n \n updateLayout(true);\n setTimeout(() => {\n- getInputField().focus();\n+ chatInputField.focus();\n },500)\n }\n \n@@ -432,7 +323,6 @@\n document.body.removeChild(overlay);\n });\n });\n- initInputField(getInputField());\n updateLayout(true);\n \n {% endblock %}"} +{"repo": ".", "date": "2025-05-17", "line": "feat: Added star animation and sandbox page", "commit": "e79abf4a26454cddf766cd1ba138554817c820cb", "diff": "commit e79abf4a26454cddf766cd1ba138554817c820cb\nAuthor: retoor \nDate: Sat May 17 17:46:59 2025 +0200\n\n Update stars.\n\ndiff --git a/src/snek/static/sandbox.css b/src/snek/static/sandbox.css\nnew file mode 100644\nindex 0000000..1419fe4\n--- /dev/null\n+++ b/src/snek/static/sandbox.css\n@@ -0,0 +1,28 @@\n+ .star {\n+ position: absolute;\n+ width: 2px;\n+ height: 2px;\n+ border-radius: 50%;\n+ opacity: 0;\n+ animation: twinkle ease-in-out infinite;\n+ }\n+\n+ @keyframes twinkle {\n+ 0%, 100% { opacity: 0; }\n+ 50% { opacity: 1; }\n+ }\n+\n+ .content {\n+ position: relative;\n+ z-index: 1;\n+ font-family: sans-serif;\n+ text-align: center;\n+ top: 40%;\n+ transform: translateY(-40%);\n+ }\n+\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 596b5d1..ceef196 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -19,6 +19,7 @@\n \n \n \n+ \n \n \n \n@@ -78,5 +79,6 @@ let installPrompt = null\n \n ;\n \n+ {% include \"sandbox.html\" %}\n \n \ndiff --git a/src/snek/templates/sandbox.html b/src/snek/templates/sandbox.html\nnew file mode 100644\nindex 0000000..6fd9b3f\n--- /dev/null\n+++ b/src/snek/templates/sandbox.html\n@@ -0,0 +1,31 @@\n+\n+\t\t\t\t\t\t\t"} +{"repo": ".", "date": "2025-01-17", "line": "feat: Initial project setup with basic files and structure", "commit": "66f89429366042c77599f3a9b8c1a7aecf976a4f", "diff": "commit 66f89429366042c77599f3a9b8c1a7aecf976a4f\nAuthor: retoor \nDate: Fri Jan 17 23:06:17 2025 +0100\n\n Initial commit.\n\ndiff --git a/.gitignore b/.gitignore\nnew file mode 100644\nindex 0000000..8cb2598\n--- /dev/null\n+++ b/.gitignore\n@@ -0,0 +1,166 @@\n+.vscode\n+.history\n+*.db*\n+\n+__pycache__/\n+*.py[cod]\n+*$py.class\n+\n+*.so\n+\n+.Python\n+build/\n+develop-eggs/\n+dist/\n+downloads/\n+eggs/\n+.eggs/\n+lib/\n+lib64/\n+parts/\n+sdist/\n+var/\n+wheels/\n+share/python-wheels/\n+*.egg-info/\n+.installed.cfg\n+*.egg\n+MANIFEST\n+\n+*.manifest\n+*.spec\n+\n+pip-log.txt\n+pip-delete-this-directory.txt\n+\n+htmlcov/\n+.tox/\n+.nox/\n+.coverage\n+.coverage.*\n+.cache\n+nosetests.xml\n+coverage.xml\n+*.cover\n+*.py,cover\n+.hypothesis/\n+.pytest_cache/\n+cover/\n+\n+*.mo\n+*.pot\n+\n+*.log\n+local_settings.py\n+db.sqlite3\n+db.sqlite3-journal\n+\n+instance/\n+.webassets-cache\n+\n+.scrapy\n+\n+docs/_build/\n+\n+.pybuilder/\n+target/\n+\n+.ipynb_checkpoints\n+\n+profile_default/\n+ipython_config.py\n+\n+\n+\n+\n+.pdm.toml\n+\n+__pypackages__/\n+\n+celerybeat-schedule\n+celerybeat.pid\n+\n+*.sage.py\n+\n+.env\n+.venv\n+env/\n+venv/\n+ENV/\n+env.bak/\n+venv.bak/\n+\n+.spyderproject\n+.spyproject\n+\n+.ropeproject\n+\n+/site\n+\n+.mypy_cache/\n+.dmypy.json\n+dmypy.json\n+\n+.pyre/\n+\n+.pytype/\n+\n+cython_debug/\n+\n+\ndiff --git a/Makefile b/Makefile\nnew file mode 100644\nindex 0000000..d41b81a\n--- /dev/null\n+++ b/Makefile\n@@ -0,0 +1,13 @@\n+PYTHON=./.venv/bin/python \n+PIP=./.venv/bin/pip \n+APP=./venv/bin/snek.serve\n+PORT = 8081\n+\n+\n+install:\n+\tpython3 -m venv .venv \n+\t$(PIP) install -e .\n+\n+run:\n+\t$(APP) --port=$(PORT)\n+\t\ndiff --git a/README.md b/README.md\nnew file mode 100644\nindex 0000000..b665fb1\n--- /dev/null\n+++ b/README.md\n@@ -0,0 +1,4 @@\n+\n+Matrix bot written in Python that says boeh everytime that Joe talks. He knows why.\n\\ No newline at end of file\ndiff --git a/pyproject.toml b/pyproject.toml\nnew file mode 100644\nindex 0000000..07de284\n--- /dev/null\n+++ b/pyproject.toml\n@@ -0,0 +1,3 @@\n+[build-system]\n+requires = [\"setuptools\", \"wheel\"]\n+build-backend = \"setuptools.build_meta\"\n\\ No newline at end of file\ndiff --git a/setup.cfg b/setup.cfg\nnew file mode 100644\nindex 0000000..bb6480d\n--- /dev/null\n+++ b/setup.cfg\n@@ -0,0 +1,24 @@\n+[metadata]\n+name = snek\n+version = 1.0.0\n+description = Snek chat server \n+author = retoor\n+author_email = retoor@molodetz.nl\n+license = MIT\n+long_description = file: README.md\n+long_description_content_type = text/markdown\n+\n+[options]\n+packages = find:\n+package_dir =\n+ = src\n+python_requires = >=3.7\n+install_requires =\n+\n+[options.packages.find]\n+where = src\n+\n+[options.entry_points]\n+console_scripts =\n+ snek.serve = snek.server:cli\ndiff --git a/src/snek/app.py b/src/snek/app.py\nnew file mode 100644\nindex 0000000..feb2fc0\n--- /dev/null\n+++ b/src/snek/app.py\n@@ -0,0 +1,20 @@\n+from app.app import Application as BaseApplication\n+from snek.forms import RegisterForm\n+from aiohttp import web\n+\n+class Application(BaseApplication):\n+\n+ def __init__(self, *args, **kwargs):\n+ super().__init__(*args, **kwargs)\n+ self.router.add_get(\"/register\", self.handle_register)\n+ self.router.add_post(\"/register\", self.handle_register)\n+\n+ async def handle_register(self, request):\n+ if request.method == \"GET\":\n+ return web.json_response({\"form\": RegisterForm().to_json()})\n+ elif request.method == \"POST\":\n+ return self.render(\"register.html\")\n+\n+if __name__ == '__main__':\n+ app = Application()\n+ web.run_app(app,port=8081,host=\"0.0.0.0\")\ndiff --git a/src/snek/forms.py b/src/snek/forms.py\nnew file mode 100644\nindex 0000000..f4a7b24\n--- /dev/null\n+++ b/src/snek/forms.py\n@@ -0,0 +1,40 @@\n+from snek import models \n+\n+class FormElement(models.ModelField):\n+\n+ def __init__(self,html_type, place_holder=None, *args, **kwargs):\n+ super().__init__(*args, **kwargs)\n+ self.place_holder = place_holder\n+ self.html_type = html_type \n+\n+ def to_json(self):\n+ data = super().to_json()\n+ data[\"html_type\"] = self.html_type\n+ data[\"place_holder\"] = self.place_holder\n+ return data \n+\n+class Form(models.BaseModel):\n+ pass\n+\n+class RegisterForm(Form):\n+\n+ username = FormElement(\n+ name=\"username\", \n+ required=True,\n+ min_length=2,\n+ max_length=20,\n+ regex=r\"^[a-zA-Z0-9_]+$\",\n+ place_holder=\"Username\",\n+ html_type=\"text\"\n+ )\n+ email = FormElement(\n+ name=\"email\",\n+ required=True,\n+ regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n+ place_holder=\"Email address\",\n+ html_type=\"email\"\n+ )\n+ password = FormElement(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",html_type=\"password\")\n+\n+\n+\ndiff --git a/src/snek/models.py b/src/snek/models.py\nnew file mode 100644\nindex 0000000..ec5d9ce\n--- /dev/null\n+++ b/src/snek/models.py\n@@ -0,0 +1,263 @@\n+import re\n+import uuid\n+import json \n+from datetime import datetime , timezone \n+from collections import OrderedDict\n+import copy \n+\n+TIMESTAMP_REGEX = r\"^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}\\+\\d{2}:\\d{2}$\"\n+\n+def now():\n+ return str(datetime.now(timezone.utc))\n+\n+def add_attrs(**kwargs):\n+ def decorator(func):\n+ for key, value in kwargs.items():\n+ setattr(func, key, value)\n+\n+ return func\n+ return decorator\n+\n+def validate_attrs(required=False,min_length=None,max_length=None,regex=None,**kwargs):\n+ def decorator(func):\n+ return add_attrs(required=required,min_length=min_length,max_length=max_length,regex=regex,**kwargs)(func)\n+\n+class Validator:\n+\n+ @property\n+ def value(self):\n+ return self._value \n+\n+ @value.setter \n+ def value(self,val):\n+ self._value = json.loads(json.dumps(val,default=str))\n+\n+ @property\n+ def initial_value(self):\n+ return None\n+\n+ def custom_validation(self):\n+ return True\n+\n+ def __init__(self,required=False,min_num=None,max_num=None,min_length=None,max_length=None,regex=None,value=None,kind=None,help_text=None,**kwargs):\n+ self.required = required \n+ self.min_num = min_num \n+ self.max_num = max_num\n+ self.min_length = min_length \n+ self.max_length = max_length \n+ self.regex = regex \n+ self._value = None \n+ self.value = value \n+ self.type = kind\n+ self.help_text = help_text \n+ self.__dict__.update(kwargs)\n+ @property \n+ def errors(self):\n+ error_list = []\n+ if self.value is None and self.required:\n+ error_list.append(\"Field is required.\")\n+ return error_list \n+ \n+ if self.value is None:\n+ return error_list \n+\n+ if self.type == float or self.type == int:\n+ if self.min_num is not None and self.value < self.min_num:\n+ error_list.append(\"Field should be minimal {}.\".format(self.min_num))\n+ if self.max_num is not None and self.value > self.max_num:\n+ error_list.append(\"Field should be maximal {}.\".format(self.max_num))\n+ if self.min_length is not None and len(self.value) < self.min_length:\n+ error_list.append(\"Field should be minimal {} characters long.\".format(self.min_length))\n+ if self.max_length is not None and len(self.value) > self.max_length:\n+ error_list.append(\"Field should be maximal {} characters long.\".format(self.max_length))\n+ if not self.regex is None and not self.value is None and not re.match(self.regex, self.value):\n+ error_list.append(\"Invalid value.\".format(self.regex))\n+ if not self.type is None and type(self.value) != self.type:\n+ error_list.append(\"Invalid type. It is supposed to be {}.\".format(self.type))\n+ return error_list \n+ \n+ def validate(self):\n+ if self.errors:\n+ raise ValueError(\"\\n\", self.errors)\n+ return True\n+\n+ @property\n+ def is_valid(self):\n+ try:\n+ self.validate()\n+ return True\n+ except ValueError:\n+ return False\n+\n+ def to_json(self):\n+ return {\n+ \"required\": self.required,\n+ \"min_num\": self.min_num,\n+ \"max_num\": self.max_num,\n+ \"min_length\": self.min_length,\n+ \"max_length\": self.max_length,\n+ \"regex\": self.regex,\n+ \"value\": self.value,\n+ \"type\": self.type,\n+ \"help_text\": self.help_text,\n+ \"errors\": self.errors,\n+ \"is_valid\": self.is_valid\n+ }\n+\n+class ModelField(Validator):\n+ def __init__(self,name=None,save=True, *args, **kwargs):\n+ self.name = name \n+ \n+ self.save = save\n+ super().__init__(*args, **kwargs)\n+\n+\n+class CreatedField(ModelField):\n+ \n+ @property\n+ def initial_value(self):\n+ return now()\n+\n+ def update(self):\n+ if not self.value:\n+ self.value = now()\n+\n+class UpdatedField(ModelField):\n+\n+ def update(self):\n+ self.value = now()\n+\n+class DeletedField(ModelField):\n+\n+ def update(self):\n+ self.value = now()\n+\n+class UUIDField(ModelField):\n+ \n+ @property \n+ def initial_value(self):\n+ return str(uuid.uuid4())\n+\n+\n+class BaseModel:\n+ \n+ uid = UUIDField(name=\"uid\",required=True)\n+ created_at = CreatedField(name=\"created_at\",required=True, regex=TIMESTAMP_REGEX, place_holder=\"Created at\")\n+ updated_at = UpdatedField(name=\"updated_at\",regex=TIMESTAMP_REGEX,place_holder=\"Updated at\")\n+ deleted_at = DeletedField(name=\"deleted_at\",regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\")\n+\n+ def __init__(self, *args, **kwargs):\n+ print(self.__dict__)\n+ print(dir(self.__class__))\n+ for key in dir(self.__class__):\n+ obj = getattr(self.__class__,key)\n+\n+ if isinstance(obj,Validator):\n+ self.__dict__[key] = copy.deepcopy(obj)\n+ print(\"JAAA\")\n+ self.__dict__[key].value = kwargs.pop(key,self.__dict__[key].initial_value)\n+\n+ def __setitem__(self, key, value):\n+ obj = self.__dict__.get(key)\n+ if isinstance(obj,Validator):\n+ obj.value = value \n+\n+ def __getattr__(self, key):\n+ obj = self.__dict__.get(key)\n+ if isinstance(obj,Validator):\n+ print(\"HPAPP\")\n+ return obj.value \n+ return obj\n+\n+\n+ def __getitem__(self, key):\n+ obj = self.__dict__.get(key)\n+ if isinstance(obj,Validator):\n+ return obj.value \n+\n+ def __setattr__(self, key, value):\n+ obj = getattr(self,key)\n+ if isinstance(obj,Validator):\n+ obj.value = value\n+ else:\n+ setattr(self,key,value)\n+ @property \n+ def record(self):\n+ obj = self.to_json()\n+ record = {}\n+ for key,value in obj.items():\n+ if getattr(self,key).save:\n+ record[key] = value.get('value')\n+ return record\n+\n+ def to_json(self,encode=False):\n+ model_data = OrderedDict({\n+ \"uid\": self.uid.value,\n+ \"created_at\": self.created_at.value,\n+ \"updated_at\": self.updated_at.value,\n+ \"deleted_at\": self.deleted_at.value\n+ })\n+ for key,value in self.__dict__.items(): \n+ if key == \"record\":\n+ continue\n+ value = self.__dict__[key]\n+ if hasattr(value,\"value\"):\n+ model_data[key] = value.to_json()\n+ if encode:\n+ return json.dumps(model_data,indent=2)\n+ return model_data\n+\n+class FormElement(ModelField):\n+\n+ def __init__(self, place_holder=None, *args, **kwargs):\n+ super().__init__(*args, **kwargs)\n+ self.place_holder = place_holder\n+\n+\n+\n+class FormElement(ModelField):\n+\n+ def __init__(self,place_holder=None, *args, **kwargs): \n+ self.place_holder = place_holder \n+ super().__init__(*args, **kwargs)\n+\n+\n+ def to_json(self):\n+ data = super().to_json()\n+ data[\"name\"] = self.name \n+ data[\"place_holder\"] = self.place_holder\n+ return data \n+\n+\n+\n+\n+class TestModel(BaseModel):\n+\n+ first_name = FormElement(name=\"first_name\",required=True,min_length=3,max_length=20,regex=r\"^[a-zA-Z0-9_]+$\",place_holder=\"First name\")\n+ last_name = FormElement(name=\"last_name\",required=True,min_length=3,max_length=20,regex=r\"^[a-zA-Z0-9_]+$\",place_holder=\"Last name\")\n+ email = FormElement(name=\"email\",required=True,regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",place_holder=\"Email address\") \n+\n+class Form:\n+ username = FormElement(required=True,min_length=3,max_length=20,regex=r\"^[a-zA-Z0-9_]+$\",place_holder=\"Username\")\n+ email = FormElement(required=True,regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",place_holder=\"Email address\")\n+ def __init__(self, *args, **kwargs):\n+ self.place_holder = kwargs.pop(\"place_holder\",None) \n+\n+\n+if __name__ == \"__main__\":\n+ model = TestModel(first_name=\"John\",last_name=\"Doe\",email=\"n9K9p@example.com\",password=\"Password123\")\n+ model2 = TestModel(first_name=\"John\",last_name=\"Doe\",email=\"ddd\",password=\"zzz\")\n+ model.first_name = \"AAA\"\n+ print(model.first_name)\n+ print(model.first_name.value)\n+ \n+ print(model.first_name)\n+ print(model.first_name.value)\n+ print(model.to_json(True))\n+ print(model2.to_json(True))\n+ print(model2.record)"} +{"repo": ".", "date": "2025-01-17", "line": "feat: Initialized project with basic description and setup instructions", "commit": "46a27405aeb8ec426fd1c686a2c090f9fe9c0e62", "diff": "commit 46a27405aeb8ec426fd1c686a2c090f9fe9c0e62\nAuthor: retoor \nDate: Fri Jan 17 23:09:46 2025 +0100\n\n Initial commit.\n\ndiff --git a/README.md b/README.md\nindex b665fb1..df001ee 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,10 @@\n \n-Matrix bot written in Python that says boeh everytime that Joe talks. He knows why.\n\\ No newline at end of file\n+This is a slack like chat application with focus on performance. Other slack-like applications became too heavy. My RocketChat just had an 'frontend' crash. Just an error from the system itself like it's the most normal thing in the world.\n+\n+At this point, there's nothing officially running but what you can do:\n+* Install the project: `make install`\n+* Run part of the project: `./venv/bin/python -m snek.app`\n+* Run other part of the project: `./venv/bin/python -m snek.models`"} +{"repo": ".", "date": "2025-01-18", "line": "feat: Added basic login and register pages with styling.", "commit": "a7446d131413da9f013a56d3541192d8ab1e22b0", "diff": "commit a7446d131413da9f013a56d3541192d8ab1e22b0\nAuthor: retoor \nDate: Sat Jan 18 13:21:38 2025 +0100\n\n Progress.\n\ndiff --git a/.gitignore b/.gitignore\nindex 8cb2598..3747073 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -1,7 +1,7 @@\n .vscode\n .history\n *.db*\n-\n+*.png\n __pycache__/\ndiff --git a/Dockerfile b/Dockerfile\nnew file mode 100644\nindex 0000000..76436ba\n--- /dev/null\n+++ b/Dockerfile\n@@ -0,0 +1,40 @@\n+FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf \n+FROM python:3.10-alpine\n+WORKDIR /code\n+ENV FLASK_APP=app.py\n+ENV FLASK_RUN_HOST=0.0.0.0\n+RUN apk add --no-cache gcc musl-dev linux-headers git\n+\n+RUN apk add --no-cache \\\n+ libstdc++ \\\n+ libx11 \\\n+ libxrender \\\n+ libxext \\\n+ libssl3 \\\n+ ca-certificates \\\n+ fontconfig \\\n+ freetype \\\n+ ttf-dejavu \\\n+ ttf-droid \\\n+ ttf-freefont \\\n+ ttf-liberation \\\n+ && apk add --no-cache --virtual .build-deps \\\n+ msttcorefonts-installer \\\n+ && update-ms-fonts \\\n+ && fc-cache -f \\\n+ && apk del .build-deps\n+COPY --from=wkhtmltopdf /bin/wkhtmltopdf /bin/wkhtmltopdf\n+COPY --from=wkhtmltopdf /bin/wkhtmltoimage /bin/wkhtmltoimage\n+COPY setup.cfg setup.cfg \n+COPY pyproject.toml pyproject.toml \n+COPY src src\n+RUN pip install --upgrade pip\n+RUN pip install -e .\n+EXPOSE 8081\n+\n+CMD [\"gunicorn\", \"-w\", \"10\", \"-k\", \"aiohttp.worker.GunicornWebWorker\", \"snek.gunicorn:app\",\"--bind\",\"0.0.0.0:8081\"]\ndiff --git a/Makefile b/Makefile\nindex d41b81a..65c58fa 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1,6 +1,8 @@\n PYTHON=./.venv/bin/python \n PIP=./.venv/bin/pip \n-APP=./venv/bin/snek.serve\n+APP=./.venv/bin/snek.serve\n+GUNICORN=./.venv/bin/gunicorn\n+GUNICORN_WORKERS = 1\n PORT = 8081\n \n \n@@ -9,5 +11,5 @@ install:\n \t$(PIP) install -e .\n \n run:\n-\t$(APP) --port=$(PORT)\n+\t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\n \t\ndiff --git a/cache/crc321300331366.cache b/cache/crc321300331366.cache\nnew file mode 100644\nindex 0000000..e69de29\ndiff --git a/cache/crc322507170282.cache b/cache/crc322507170282.cache\nnew file mode 100644\nindex 0000000..e69de29\ndiff --git a/compose.yml b/compose.yml\nnew file mode 100644\nindex 0000000..9186108\n--- /dev/null\n+++ b/compose.yml\n@@ -0,0 +1,12 @@\n+services:\n+ snek:\n+ build: .\n+ ports:\n+ - \"8081:8081\"\n+ volumes:\n+ - ./:/code\n+ develop:\n+ watch:\n+ - action: sync\n+ path: .\n+ target: /code\n\\ No newline at end of file\ndiff --git a/setup.cfg b/setup.cfg\nindex bb6480d..ca8353d 100644\n--- a/setup.cfg\n+++ b/setup.cfg\n@@ -15,6 +15,10 @@ package_dir =\n python_requires = >=3.7\n install_requires =\n+ beautifulsoup4\n+ gunicorn\n+ imgkit\n+ wkhtmltopdf\n \n [options.packages.find]\n where = src\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex feb2fc0..97fbb96 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -1,20 +1,60 @@\n from app.app import Application as BaseApplication\n from snek.forms import RegisterForm\n from aiohttp import web\n+import aiohttp \n+import pathlib\n+from snek import http \n+from snek.middleware import cors_allow_middleware,cors_middleware\n+\n \n class Application(BaseApplication):\n \n def __init__(self, *args, **kwargs):\n- super().__init__(*args, **kwargs)\n+ middlewares = [\n+ cors_middleware,\n+ web.normalize_path_middleware(merge_slashes=True)\n+ ]\n+ self.template_path = pathlib.Path(__file__).parent.joinpath(\"templates\")\n+ super().__init__(middlewares=middlewares, template_path=self.template_path,*args, **kwargs)\n+ self.router.add_static(\"/\",pathlib.Path(__file__).parent.joinpath(\"static\"),name=\"static\",show_index=True)\n self.router.add_get(\"/register\", self.handle_register)\n+ self.router.add_get(\"/login\", self.handle_login)\n+ self.router.add_get(\"/test\", self.handle_test)\n self.router.add_post(\"/register\", self.handle_register)\n+ self.router.add_get(\"/http-get\",self.handle_http_get)\n+ self.router.add_get(\"/http-photo\",self.handle_http_photo)\n+\n+ async def handle_test(self,request):\n+\n+ return await self.render_template(\"test.html\",request,context={\"name\":\"retoor\"})\n+\n+ async def handle_http_get(self, request:web.Request):\n+ url = request.query.get(\"url\")\n+ content = await http.get(url)\n+ return web.Response(body=content)\n+\n+ async def handle_http_photo(self, request):\n+ url = request.query.get(\"url\")\n+ path = await http.create_site_photo(url)\n+ return web.Response(body=path.read_bytes(),headers={\n+ \"Content-Type\": \"image/png\"\n+ })\n+\n+ async def handle_login(self, request):\n+ if request.method == \"GET\":\n+ elif request.method == \"POST\":\n+ \n \n async def handle_register(self, request):\n if request.method == \"GET\":\n- return web.json_response({\"form\": RegisterForm().to_json()})\n elif request.method == \"POST\":\n return self.render(\"register.html\")\n \n+app = Application()\n+\n if __name__ == '__main__':\n- app = Application()\n+\n web.run_app(app,port=8081,host=\"0.0.0.0\")\ndiff --git a/src/snek/gunicorn.py b/src/snek/gunicorn.py\nnew file mode 100644\nindex 0000000..4055bbd\n--- /dev/null\n+++ b/src/snek/gunicorn.py\n@@ -0,0 +1,3 @@\n+from snek.app import app \n+\n+application = app\ndiff --git a/src/snek/http.py b/src/snek/http.py\nnew file mode 100644\nindex 0000000..0b16bee\n--- /dev/null\n+++ b/src/snek/http.py\n@@ -0,0 +1,83 @@\n+from aiohttp import web \n+import aiohttp \n+from app.cache import time_cache_async\n+from bs4 import BeautifulSoup\n+from urllib.parse import urljoin\n+import pathlib \n+import uuid \n+import imgkit \n+import asyncio\n+import zlib\n+import io \n+\n+async def crc32(data):\n+ try:\n+ data = data.encode()\n+ except:\n+ pass \n+ result = \"crc32\" + str(zlib.crc32(data))\n+ return result \n+\n+async def get_file(name,suffix=\".cache\"):\n+ name = await crc32(name)\n+ path = pathlib.Path(\".\").joinpath(\"cache\")\n+ if not path.exists():\n+ path.mkdir(parents=True,exist_ok=True)\n+ path = path.joinpath(name + suffix)\n+ return path\n+\n+\n+\n+async def public_touch(name=None):\n+ path = pathlib.Path(\".\").joinpath(str(uuid.uuid4())+name)\n+ path.open(\"wb\").close()\n+ return path \n+\n+async def create_site_photo(url):\n+ loop = asyncio.get_event_loop()\n+ if not url.startswith(\"https\"):\n+ output_path = await get_file(\"site-screenshot-\" + url,\".png\")\n+ \n+ if output_path.exists():\n+ return output_path\n+ output_path.touch()\n+ def make_photo():\n+ imgkit.from_url(url, output_path.absolute())\n+ return output_path \n+\n+ return await loop.run_in_executor(None,make_photo)\n+\n+async def repair_links(base_url, html_content):\n+ soup = BeautifulSoup(html_content, \"html.parser\")\n+ for tag in soup.find_all(['a', 'img', 'link']):\n+ tag['href'] = urljoin(base_url, tag['href'])\n+ tag['src'] = urljoin(base_url, tag['src'])\n+ print(\"Fixed: \",tag['src'])\n+ return soup.prettify()\n+\n+async def is_html_content(content: bytes):\n+ try:\n+ content = content.decode(errors='ignore')\n+ except:\n+ pass \n+ marks = ['\n+\n+\n+ \n+ \n+ Register\n+ \n+\n+\n+
\n+

Login

\n+ \n+ \n+ \n+ \n+ Not having an account yet? Register here.\n+ \n+
\n+\n+\ndiff --git a/src/snek/templates/prachtig_gitter_like.html b/src/snek/templates/prachtig_gitter_like.html\nnew file mode 100644\nindex 0000000..cd2d863\n--- /dev/null\n+++ b/src/snek/templates/prachtig_gitter_like.html\n@@ -0,0 +1,51 @@\n+\n+\n+\n+ \n+ \n+ Dark Themed Chat Application\n+ \n+\n+\n+
\n+
Molodetz Chat
\n+ \n+
\n+
\n+ \n+
\n+
\n+

General

\n+
\n+
\n+
\n+ Alice:\n+ Hello, everyone!\n+
\n+
\n+ Bob:\n+ Hi Alice! How are you?\n+
\n+
\n+
\n+ \n+ \n+
\n+
\n+
\n+\n+\n+\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nnew file mode 100644\nindex 0000000..da41629\n--- /dev/null\n+++ b/src/snek/templates/register.html\n@@ -0,0 +1,22 @@\n+\n+\n+\n+ \n+ \n+ Register\n+ \n+\n+\n+
\n+

Register

\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+\n+\ndiff --git a/src/snek/templates/test.html b/src/snek/templates/test.html\nnew file mode 100644\nindex 0000000..c23103a\n--- /dev/null\n+++ b/src/snek/templates/test.html\n@@ -0,0 +1,63 @@\n+\n+\n+\n+ \n+ \n+ Dark Themed Chat Application\n+ \n+ \n+ \n+\n+\n+
\n+
Molodetz Chat
\n+ \n+
\n+
\n+ \n+
\n+
\n+

General

\n+
\n+
\n+
\n+
A
\n+
\n+
Alice
\n+
Hello, everyone!
\n+
10:45 AM
\n+
\n+
\n+ \n+
\n+
B
\n+
\n+
Bob
\n+
Hi Alice! How are you?
\n+
10:46 AM
\n+
\n+
\n+
\n+
\n+ \n+ \n+
\n+
\n+
\n+ \n+\n+\n+\ndiff --git a/src/snek/templates/test2.html b/src/snek/templates/test2.html\nnew file mode 100644\nindex 0000000..9aad83a\n--- /dev/null\n+++ b/src/snek/templates/test2.html\n@@ -0,0 +1,122 @@\n+\n+\n+\n+ \n+ \n+ Dynamic Form Component\n+ \n+\n+\n+ \n+ \n+\n+ \n+\n+\n+"} +{"repo": ".", "date": "2025-01-18", "line": "feat: Added restart policy to snek service", "commit": "2e3b85d7f739160783e7c5552f1306298047704a", "diff": "commit 2e3b85d7f739160783e7c5552f1306298047704a\nAuthor: retoor \nDate: Sat Jan 18 12:23:23 2025 +0000\n\n Updated compose.yml\n\ndiff --git a/compose.yml b/compose.yml\nindex 9186108..776e60d 100644\n--- a/compose.yml\n+++ b/compose.yml\n@@ -1,12 +1,8 @@\n services:\n snek:\n build: .\n+ restart: always\n ports:\n - \"8081:8081\"\n volumes:\n - ./:/code\n- develop:\n- watch:\n- - action: sync\n- path: .\n- target: /code\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor project structure and introduce generic form components.", "commit": "ba83922660dade77dcb96e8ba9c73cfcba8c2b81", "diff": "commit ba83922660dade77dcb96e8ba9c73cfcba8c2b81\nAuthor: retoor \nDate: Fri Jan 24 03:28:43 2025 +0100\n\n Progress.\n\ndiff --git a/src/snek/static/prachtig-gitter_like.html b/.resources/prachtig-gitter_like.html\nsimilarity index 100%\nrename from src/snek/static/prachtig-gitter_like.html\nrename to .resources/prachtig-gitter_like.html\ndiff --git a/src/snek/templates/prachtig_gitter_like.html b/.resources/prachtig_gitter_like.html\nsimilarity index 100%\nrename from src/snek/templates/prachtig_gitter_like.html\nrename to .resources/prachtig_gitter_like.html\ndiff --git a/Dockerfile b/Dockerfile\nindex 76436ba..47c0ece 100644\n--- a/Dockerfile\n+++ b/Dockerfile\n@@ -37,4 +37,5 @@ RUN pip install --upgrade pip\n RUN pip install -e .\n EXPOSE 8081\n \n-CMD [\"gunicorn\", \"-w\", \"10\", \"-k\", \"aiohttp.worker.GunicornWebWorker\", \"snek.gunicorn:app\",\"--bind\",\"0.0.0.0:8081\"]\n+python -m snek.app\ndiff --git a/LICENSE.txt b/LICENSE.txt\nnew file mode 100644\nindex 0000000..6b0da88\n--- /dev/null\n+++ b/LICENSE.txt\n@@ -0,0 +1,21 @@\n+MIT License\n+\n+Copyright (c) 2025 retoor\n+\n+Permission is hereby granted, free of charge, to any person obtaining a copy\n+of this software and associated documentation files (the \"Software\"), to deal\n+in the Software without restriction, including without limitation the rights\n+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n+copies of the Software, and to permit persons to whom the Software is\n+furnished to do so, subject to the following conditions:\n+\n+The above copyright notice and this permission notice shall be included in all\n+copies or substantial portions of the Software.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n+SOFTWARE.\ndiff --git a/Makefile b/Makefile\nindex 65c58fa..f153fc1 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -10,6 +10,8 @@ install:\n \tpython3 -m venv .venv \n \t$(PIP) install -e .\n \n+\n+\n run:\n \t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\n \t\ndiff --git a/compose.yml b/compose.yml\nindex 776e60d..3b1f650 100644\n--- a/compose.yml\n+++ b/compose.yml\n@@ -6,3 +6,5 @@ services:\n - \"8081:8081\"\n volumes:\n - ./:/code\n+ entrypoint: [\"python\",\"-m\",\"snek.app\"]\n+ \n\\ No newline at end of file\ndiff --git a/pyproject.toml b/pyproject.toml\nindex 07de284..d98557e 100644\n--- a/pyproject.toml\n+++ b/pyproject.toml\n@@ -1,3 +1,25 @@\n [build-system]\n requires = [\"setuptools\", \"wheel\"]\n-build-backend = \"setuptools.build_meta\"\n\\ No newline at end of file\n+build-backend = \"setuptools.build_meta\"\n+\n+[project]\n+name = \"Snek\"\n+version = \"1.0.0\"\n+readme = \"README.md\"\n+license = { file = \"LICENSE\", content-type=\"text/markdown\" }\n+description = \"Snek Chat Application by Molodetz\"\n+authors = [\n+ { name = \"retoor\", email = \"retoor@molodetz.nl\" }\n+]\n+keywords = [\"chat\", \"snek\", \"molodetz\"]\n+requires-python = \">=3.12\"\n+dependencies = [\n+ \"mkdocs>=1.4.0\",\n+ \"shed\",\n+ \"beautifulsoup4\",\n+ \"gunicorn\",\n+ \"imgkit\",\n+ \"wkhtmltopdf\"\n+]\n+\ndiff --git a/setup.cfg b/setup.cfg\nindex ca8353d..045fc92 100644\n--- a/setup.cfg\n+++ b/setup.cfg\n@@ -19,6 +19,7 @@ install_requires =\n gunicorn\n imgkit\n wkhtmltopdf\n+ shed\n \n [options.packages.find]\n where = src\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 97fbb96..0e2ed63 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -1,10 +1,16 @@\n-from app.app import Application as BaseApplication\n-from snek.forms import RegisterForm\n-from aiohttp import web\n-import aiohttp \n import pathlib\n-from snek import http \n-from snek.middleware import cors_allow_middleware,cors_middleware\n+\n+from aiohttp import web\n+from app.app import Application as BaseApplication\n+\n+from snek.system import http\n+from snek.system.middleware import cors_middleware\n+from snek.view.index import IndexView\n+from snek.view.login import LoginView\n+from snek.view.login_form import LoginFormView\n+from snek.view.register import RegisterView\n+from snek.view.register_form import RegisterFormView\n+from snek.view.view import WebView\n \n \n class Application(BaseApplication):\n@@ -12,23 +18,38 @@ class Application(BaseApplication):\n def __init__(self, *args, **kwargs):\n middlewares = [\n cors_middleware,\n- web.normalize_path_middleware(merge_slashes=True)\n+ web.normalize_path_middleware(merge_slashes=True),\n ]\n self.template_path = pathlib.Path(__file__).parent.joinpath(\"templates\")\n- super().__init__(middlewares=middlewares, template_path=self.template_path,*args, **kwargs)\n- self.router.add_static(\"/\",pathlib.Path(__file__).parent.joinpath(\"static\"),name=\"static\",show_index=True)\n- self.router.add_get(\"/register\", self.handle_register)\n- self.router.add_get(\"/login\", self.handle_login)\n- self.router.add_get(\"/test\", self.handle_test)\n- self.router.add_post(\"/register\", self.handle_register)\n- self.router.add_get(\"/http-get\",self.handle_http_get)\n- self.router.add_get(\"/http-photo\",self.handle_http_photo)\n+ super().__init__(\n+ middlewares=middlewares, template_path=self.template_path, *args, **kwargs\n+ )\n+ self.setup_router()\n+\n+ def setup_router(self):\n+ self.router.add_get(\"/\", IndexView)\n+ self.router.add_static(\n+ \"/\",\n+ pathlib.Path(__file__).parent.joinpath(\"static\"),\n+ name=\"static\",\n+ show_index=True,\n+ )\n+ self.router.add_view(\"/web\", WebView)\n+ self.router.add_view(\"/login\", LoginView)\n+ self.router.add_view(\"/login-form\", LoginFormView)\n+ self.router.add_view(\"/register\", RegisterView)\n+ \n+ self.router.add_view(\"/register-form\", RegisterFormView)\n+ self.router.add_get(\"/http-get\", self.handle_http_get)\n+ self.router.add_get(\"/http-photo\", self.handle_http_photo)\n \n- async def handle_test(self,request):\n+ async def handle_test(self, request):\n \n- return await self.render_template(\"test.html\",request,context={\"name\":\"retoor\"})\n+ return await self.render_template(\n+ \"test.html\", request, context={\"name\": \"retoor\"}\n+ )\n \n- async def handle_http_get(self, request:web.Request):\n+ async def handle_http_get(self, request: web.Request):\n url = request.query.get(\"url\")\n content = await http.get(url)\n return web.Response(body=content)\n@@ -36,25 +57,13 @@ class Application(BaseApplication):\n async def handle_http_photo(self, request):\n url = request.query.get(\"url\")\n path = await http.create_site_photo(url)\n- return web.Response(body=path.read_bytes(),headers={\n- \"Content-Type\": \"image/png\"\n- })\n-\n- async def handle_login(self, request):\n- if request.method == \"GET\":\n- elif request.method == \"POST\":\n- \n+ return web.Response(\n+ body=path.read_bytes(), headers={\"Content-Type\": \"image/png\"}\n+ )\n \n- async def handle_register(self, request):\n- if request.method == \"GET\":\n- elif request.method == \"POST\":\n- return self.render(\"register.html\")\n \n app = Application()\n \n-if __name__ == '__main__':\n+if __name__ == \"__main__\":\n \n- web.run_app(app,port=8081,host=\"0.0.0.0\")\n+ web.run_app(app, port=8081, host=\"0.0.0.0\")\ndiff --git a/src/snek/form/__init__.py b/src/snek/form/__init__.py\nnew file mode 100644\nindex 0000000..e69de29\ndiff --git a/src/snek/form/login.py b/src/snek/form/login.py\nnew file mode 100644\nindex 0000000..a87f7d3\n--- /dev/null\n+++ b/src/snek/form/login.py\n@@ -0,0 +1,24 @@\n+from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement\n+\n+class LoginForm(Form):\n+\n+ title = HTMLElement(tag=\"h1\", text=\"Login\")\n+\n+ username = FormInputElement(\n+ name=\"username\", \n+ required=True,\n+ min_length=2,\n+ max_length=20,\n+ regex=r\"^[a-zA-Z0-9_]+$\",\n+ place_holder=\"Username\",\n+ type=\"text\"\n+ )\n+ password = FormInputElement(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\",type=\"password\",place_holder=\"Password\")\n+\n+ action = FormButtonElement(\n+ name=\"action\",\n+ value=\"submit\",\n+ text=\"Login\",\n+ type=\"button\"\n+ )\n+\ndiff --git a/src/snek/form/register.py b/src/snek/form/register.py\nnew file mode 100644\nindex 0000000..60399fb\n--- /dev/null\n+++ b/src/snek/form/register.py\n@@ -0,0 +1,31 @@\n+from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement\n+\n+class RegisterForm(Form):\n+\n+ title = HTMLElement(tag=\"h1\", text=\"Register\")\n+\n+ username = FormInputElement(\n+ name=\"username\", \n+ required=True,\n+ min_length=2,\n+ max_length=20,\n+ regex=r\"^[a-zA-Z0-9_]+$\",\n+ place_holder=\"Username\",\n+ type=\"text\"\n+ )\n+ email = FormInputElement(\n+ name=\"email\",\n+ required=True,\n+ regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n+ place_holder=\"Email address\",\n+ type=\"email\"\n+ )\n+ password = FormInputElement(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\",type=\"password\",place_holder=\"Password\")\n+\n+ action = FormButtonElement(\n+ name=\"action\",\n+ value=\"submit\",\n+ text=\"Register\",\n+ type=\"button\"\n+ )\n+\ndiff --git a/src/snek/forms.py b/src/snek/forms.py\ndeleted file mode 100644\nindex f4a7b24..0000000\n--- a/src/snek/forms.py\n+++ /dev/null\n@@ -1,40 +0,0 @@\n-from snek import models \n-\n-class FormElement(models.ModelField):\n-\n- def __init__(self,html_type, place_holder=None, *args, **kwargs):\n- super().__init__(*args, **kwargs)\n- self.place_holder = place_holder\n- self.html_type = html_type \n-\n- def to_json(self):\n- data = super().to_json()\n- data[\"html_type\"] = self.html_type\n- data[\"place_holder\"] = self.place_holder\n- return data \n-\n-class Form(models.BaseModel):\n- pass\n-\n-class RegisterForm(Form):\n-\n- username = FormElement(\n- name=\"username\", \n- required=True,\n- min_length=2,\n- max_length=20,\n- regex=r\"^[a-zA-Z0-9_]+$\",\n- place_holder=\"Username\",\n- html_type=\"text\"\n- )\n- email = FormElement(\n- name=\"email\",\n- required=True,\n- regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n- place_holder=\"Email address\",\n- html_type=\"email\"\n- )\n- password = FormElement(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",html_type=\"password\")\n-\n-\n-\ndiff --git a/src/snek/gunicorn.py b/src/snek/gunicorn.py\nindex 4055bbd..8583142 100644\n--- a/src/snek/gunicorn.py\n+++ b/src/snek/gunicorn.py\n@@ -1,3 +1,3 @@\n-from snek.app import app \n+from snek.app import app\n \n application = app\ndiff --git a/src/snek/model/__init__.py b/src/snek/model/__init__.py\nnew file mode 100644\nindex 0000000..e69de29\ndiff --git a/src/snek/model/user.py b/src/snek/model/user.py\nnew file mode 100644\nindex 0000000..44553f8\n--- /dev/null\n+++ b/src/snek/model/user.py\n@@ -0,0 +1,19 @@\n+from snek.system.model import BaseModel,ModelField\n+\n+class User(BaseModel):\n+ \n+ username = ModelField(\n+ name=\"username\", \n+ required=True,\n+ min_length=2,\n+ max_length=20,\n+ regex=r\"^[a-zA-Z0-9_]+$\",\n+ )\n+ email = ModelField(\n+ name=\"email\",\n+ required=True,\n+ regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\"\n+ )\n+ password = ModelField(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\")\n+\n+\ndiff --git a/src/snek/static/app.js b/src/snek/static/app.js\nindex 701beda..f06ee24 100644\n--- a/src/snek/static/app.js\n+++ b/src/snek/static/app.js\n@@ -57,13 +57,26 @@ class Room {\n this.name = name \n }\n setMessages(list){\n- \n+\n }\n \n \n }\n \n \n+class InlineAppElement extends HTMLElement {\n+ \n+ constructor(){\n+ this.\n+ }\n+\n+}\n+\n+class Page {\n+ elements = []\n+\n+}\n+\n class App {\n rooms = []\n constructor() {\ndiff --git a/src/snek/static/styles.css b/src/snek/static/base.css\nsimilarity index 94%\nrename from src/snek/static/styles.css\nrename to src/snek/static/base.css\nindex 83c1fb1..363e4f1 100644\n--- a/src/snek/static/styles.css\n+++ b/src/snek/static/base.css\n@@ -1,11 +1,9 @@\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n body {\n font-family: Arial, sans-serif;\n@@ -16,7 +14,6 @@ body {\n height: 100vh;\n }\n \n header {\n padding: 10px 20px;\n@@ -43,14 +40,12 @@ header nav a:hover {\n }\n \n main {\n display: flex;\n flex: 1;\n overflow: hidden;\n }\n \n .sidebar {\n width: 250px;\n@@ -84,7 +79,6 @@ main {\n }\n \n .chat-area {\n flex: 1;\n display: flex;\n@@ -103,7 +97,6 @@ main {\n }\n \n .chat-messages {\n flex: 1;\n padding: 20px;\n@@ -155,7 +148,6 @@ main {\n }\n \n .chat-input {\n padding: 15px;\n@@ -190,7 +182,6 @@ main {\n }\n \n @media (max-width: 768px) {\n .sidebar {\n display: none;\ndiff --git a/src/snek/static/fancy-button.js b/src/snek/static/fancy-button.js\nnew file mode 100644\nindex 0000000..5407a3b\n--- /dev/null\n+++ b/src/snek/static/fancy-button.js\n@@ -0,0 +1,54 @@\n+\n+\n+class FancyButton extends HTMLElement {\n+ url = null\n+ type=\"button\"\n+ value = null\n+ constructor(){\n+ super()\n+ this.attachShadow({mode:'open'})\n+ this.container = document.createElement('span')\n+ this.styleElement = document.createElement(\"style\")\n+ this.styleElement.innerHTML = `\n+ :root {\n+ width:100%;\n+ --width: 100%;\n+ }\n+ button {\n+ width: var(--width);\n+ min-width: 33%;\n+ padding: 10px;\n+ border: none;\n+ border-radius: 5px;\n+ color: white;\n+ font-size: 1em;\n+ font-weight: bold;\n+ cursor: pointer;\n+ transition: background-color 0.3s;\n+ }\n+ button:hover {\n+ }\n+ `\n+ this.container.appendChild(this.styleElement)\n+ this.buttonElement = document.createElement('button')\n+ this.container.appendChild(this.buttonElement)\n+ this.shadowRoot.appendChild(this.container)\n+ }\n+\n+ connectedCallback() {\n+ this.url = this.getAttribute('url');\n+ this.value = this.getAttribute('value')\n+ const me = this \n+ this.buttonElement.appendChild(document.createTextNode(this.getAttribute(\"text\")))\n+ this.buttonElement.addEventListener(\"click\",()=>{\n+ if(me.url){\n+ window.location = me.url\n+ }\n+ })\n+ }\n+}\n+\n+customElements.define(\"fancy-button\",FancyButton)\ndiff --git a/src/snek/static/generic-form.css b/src/snek/static/generic-form.css\nnew file mode 100644\nindex 0000000..d593816\n--- /dev/null\n+++ b/src/snek/static/generic-form.css\n@@ -0,0 +1,100 @@\n+* {\n+ margin: 0;\n+ padding: 0;\n+ box-sizing: border-box;\n+ }\n+ \n+ body {\n+ font-family: Arial, sans-serif;\n+ line-height: 1.5;\n+ display: flex;\n+ flex-direction: column;\n+ justify-content: center;\n+ align-items: center;\n+ height: 100vh;\n+ }\n+\n+generic-form {\n+ margin: 0;\n+ padding: 0;\n+ box-sizing: border-box;\n+\n+}\n+\n+.generic-form-container {\n+ \n+ border-radius: 10px;\n+ padding: 30px;\n+ width: 400px;\n+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n+ text-align: center;\n+\n+}\n+\n+.generic-form-container h1 {\n+ font-size: 2em;\n+ margin-bottom: 20px;\n+}\n+input {\n+\n+}\n+.generic-form-container generic-field {\n+ width: 100%;\n+ padding: 10px;\n+ margin: 10px 0;\n+ border-radius: 5px;\n+ font-size: 1em;\n+}\n+\n+.generic-form-container button {\n+ width: 100%;\n+ padding: 10px;\n+ border: none;\n+ border-radius: 5px;\n+ color: white;\n+ font-size: 1em;\n+ font-weight: bold;\n+ cursor: pointer;\n+ transition: background-color 0.3s;\n+}\n+\n+.generic-form-container button:hover {\n+}\n+\n+.generic-form-container a {\n+ text-decoration: none;\n+ display: block;\n+ margin-top: 15px;\n+ font-size: 0.9em;\n+ transition: color 0.3s;\n+}\n+\n+.generic-form-container a:hover {\n+}\n+\n+\n+.error {\n+ font-size: 0.9em;\n+ margin-top: 5px;\n+}\n+\n+\n+@media (max-width: 500px) {\n+ .generic-form-container {\n+ width: 90%;\n+ }\n+}\n\\ No newline at end of file\ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nnew file mode 100644\nindex 0000000..11fea47\n--- /dev/null\n+++ b/src/snek/static/generic-form.js\n@@ -0,0 +1,321 @@\n+\n+class GenericField extends HTMLElement {\n+ form = null\n+ field = null \n+ inputElement = null\n+ footerElement = null \n+ action = null \n+ container = null\n+ styleElement = null\n+ name = null\n+ get value() {\n+ return this.inputElement.value\n+ }\n+ get type() {\n+\n+ return this.field.tag \n+ }\n+ set value(val) {\n+ val = val == null ? '' : val \n+ this.inputElement.value = val \n+ this.inputElement.setAttribute(\"value\", val)\n+ }\n+ setInvalid(){\n+ this.inputElement.classList.add(\"error\")\n+ this.inputElement.classList.remove(\"valid\")\n+ }\n+ setErrors(errors){\n+ if(errors.length)\n+ this.inputElement.setAttribute(\"title\", errors[0])\n+ else\n+ this.inputElement.setAttribute(\"title\",\"\")\n+ }\n+ setValid(){\n+ this.inputElement.classList.remove(\"error\")\n+ this.inputElement.classList.add(\"valid\")\n+ }\n+ constructor() {\n+ super()\n+ this.attachShadow({mode:'open'})\n+ this.container = document.createElement('div')\n+ this.styleElement = document.createElement('style')\n+ this.styleElement.innerHTML = `\n+\n+ h1 {\n+ font-size: 2em;\n+ margin-bottom: 20px;\n+ margin-top: 0px;\n+ }\n+\n+ input {\n+ width: 90%;\n+ padding: 10px;\n+ margin: 10px 0;\n+ border-radius: 5px;\n+ font-size: 1em;\n+ }\n+\n+ button {\n+ width: 50%;\n+ padding: 10px;\n+ border: none;\n+ float: right;\n+ margin-top: 10px;\n+ margin-left: 10px;\n+ margin-right: 10px;\n+ border-radius: 5px;\n+ color: white;\n+ font-size: 1em;\n+ font-weight: bold;\n+ cursor: pointer;\n+ transition: background-color 0.3s;\n+ clear: both;\n+ }\n+\n+ button:hover {\n+ }\n+\n+ a {\n+ text-decoration: none;\n+ display: block;\n+ margin-top: 15px;\n+ font-size: 0.9em;\n+ transition: color 0.3s;\n+ }\n+\n+ a:hover {\n+ }\n+ .valid {\n+ border: 1px solid green;\n+ color:green;\n+ font-size: 0.9em;\n+ margin-top: 5px;\n+ }\n+ .error {\n+ border: 3px solid red;\n+ font-size: 0.9em;\n+ margin-top: 5px;\n+ }\n+ @media (max-width: 500px) {\n+ input {\n+ width: 90%;\n+ }\n+ } \n+ \n+ `\n+ this.container.appendChild(this.styleElement)\n+ \n+ this.shadowRoot.appendChild(this.container)\n+ }\n+ connectedCallback(){\n+\n+ this.updateAttributes() \n+ \n+ }\n+ setAttribute(name,value){\n+ this[name] = value \n+ }\n+ updateAttributes(){\n+ if(this.inputElement == null && this.field){\n+ this.inputElement = document.createElement(this.field.tag)\n+ if(this.field.tag == 'button'){\n+ if(this.field.value == \"submit\"){\n+ \n+ \n+ }\n+ this.action = this.field.value\n+ }\n+ this.inputElement.name = this.field.name \n+ this.name = this.inputElement.name\n+ const me = this\n+ this.inputElement.addEventListener(\"keyup\",(e)=>{\n+ if(e.key == 'Enter'){\n+ me.dispatchEvent(new Event(\"submit\"))\n+ }else if(me.field.value != e.target.value)\n+ {\n+ const event = new CustomEvent(\"change\", {detail:me,bubbles:true})\n+ me.dispatchEvent(event)\n+ }\n+ })\n+ this.inputElement.addEventListener(\"click\",(e)=>{\n+ const event = new CustomEvent(\"click\",{detail:me,bubbles:true})\n+ me.dispatchEvent(event) \n+ })\n+ this.container.appendChild(this.inputElement)\n+\n+}\n+ if(!this.field){\n+ return\n+ }\n+ this.inputElement.setAttribute(\"type\",this.field.type == null ? 'input' : this.field.type)\n+ this.inputElement.setAttribute(\"name\",this.field.name == null ? '' : this.field.name)\n+ \n+ if(this.field.text != null){\n+ this.inputElement.innerText = this.field.text \n+ }\n+ if(this.field.html != null){\n+ this.inputElement.innerHTML = this.field.html\n+ }\n+ if(this.field.class_name){\n+ this.inputElement.classList.add(this.field.class_name)\n+ }\n+ this.inputElement.setAttribute(\"tabindex\", this.field.index)\n+ this.inputElement.classList.add(this.field.name)\n+ this.value = this.field.value\n+ let place_holder = null \n+ if(this.field.place_holder)\n+ place_holder = this.field.place_holder\n+ if(this.field.required && place_holder){\n+ place_holder = place_holder\n+ }\n+ if(place_holder)\n+ this.field.place_holder = \"* \" + place_holder\n+ this.inputElement.setAttribute(\"placeholder\",place_holder)\n+ if(this.field.required)\n+ this.inputElement.setAttribute(\"required\",\"required\")\n+ else\n+ this.inputElement.removeAttribute(\"required\")\n+ if(!this.footerElement){\n+ this.footerElement = document.createElement('div')\n+ this.footerElement.style.clear = 'both'\n+ this.container.appendChild(this.footerElement)\n+ }\n+ }\n+}\n+\n+customElements.define('generic-field', GenericField);\n+\n+class GenericForm extends HTMLElement {\n+ fields = {} \n+ form = {}\n+ constructor() {\n+\n+\n+ super();\n+ this.attachShadow({ mode: 'open' });\n+ this.styleElement = document.createElement(\"style\")\n+ this.styleElement.innerHTML = `\n+\n+ * {\n+ margin: 0;\n+ padding: 0;\n+ box-sizing: border-box;\n+ width:90%\n+\n+ }\n+\n+ div {\n+ \n+ border-radius: 10px;\n+ padding: 30px;\n+ width: 400px;\n+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n+ text-align: center;\n+\n+ }\n+ @media (max-width: 500px) {\n+ form {\n+ width: 80%;\n+ }\n+ }`\n+ \n+ this.container = document.createElement('div');\n+ this.container.appendChild(this.styleElement)\n+ this.container.classList.add(\"generic-form-container\")\n+ this.shadowRoot.appendChild(this.container);\n+ }\n+\n+ connectedCallback() {\n+ const url = this.getAttribute('url');\n+ if (url) {\n+ const fullUrl = url.startsWith(\"/\") ? window.location.origin + url : new URL(window.location.origin + \"/http-get\")\n+ if(!url.startsWith(\"/\"))\n+ fullUrl.searchParams.set('url', url) \n+ this.loadForm(fullUrl.toString());\n+ } else {\n+ this.container.textContent = \"No URL provided!\";\n+ }\n+ }\n+\n+ async loadForm(url) {\n+ const me = this \n+ try {\n+ const response = await fetch(url);\n+ if (!response.ok) {\n+ throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);\n+ }\n+ me.form = await response.json();\n+ \n+ let fields = Object.values(me.form.fields)\n+ \n+ fields = fields.sort((a,b)=>{\n+ console.info(a.index,b.index)\n+ return a.index - b.index\n+ }) \n+ fields.forEach(field=>{\n+ const fieldElement = document.createElement('generic-field')\n+ me.fields[field.name] = fieldElement \n+ fieldElement.setAttribute(\"form\", me)\n+ fieldElement.setAttribute(\"field\", field)\n+ me.container.appendChild(fieldElement)\n+ fieldElement.updateAttributes() \n+ fieldElement.addEventListener(\"change\",(e)=>{\n+ me.form.fields[e.detail.name].value = e.detail.value\n+ })\n+ fieldElement.addEventListener(\"click\",async (e)=>{\n+ if(e.detail.type == \"button\"){\n+ if(e.detail.value == \"submit\")\n+ {\n+ await me.validate()\n+ }\n+ }\n+ \n+ })\n+ })\n+ \n+ } catch (error) {\n+ this.container.textContent = `Error: ${error.message}`;\n+ }\n+ }\n+ async validate(){\n+ const url = this.getAttribute(\"url\")\n+ const me = this\n+ const response = await fetch(url,{\n+ method: 'POST',\n+ headers: {\n+ 'Content-Type': 'application/json'\n+ },\n+ body: JSON.stringify({\"action\":\"validate\", \"form\":me.form})\n+ });\n+ const form = await response.json()\n+ Object.values(form.fields).forEach(field=>{\n+ if(!me.form.fields[field.name])\n+ return\n+ me.form.fields[field.name].is_valid = field.is_valid \n+ if(!field.is_valid){\n+ me.fields[field.name].setInvalid()\n+ me.fields[field.name].setErrors(field.errors)\n+ console.info(field.name,\"is invalid\")\n+ }else{\n+ me.fields[field.name].setValid()\n+ }\n+ me.fields[field.name].setAttribute(\"field\",field)\n+ me.fields[field.name].updateAttributes()\n+ })\n+ Object.values(form.fields).forEach(field=>{\n+ console.info(field.errors)\n+ me.fields[field.name].setErrors(field.errors)\n+ })\n+ }\n+ }\n+ customElements.define('generic-form', GenericForm);\n\\ No newline at end of file\ndiff --git a/src/snek/static/html_frame.css b/src/snek/static/html-frame.css\nsimilarity index 53%\nrename from src/snek/static/html_frame.css\nrename to src/snek/static/html-frame.css\nindex 6b64c76..92a1f97 100644\n--- a/src/snek/static/html_frame.css\n+++ b/src/snek/static/html-frame.css\n@@ -1,9 +1,6 @@\n .html-frame {\n width: 100px;\n height: 50px;\n- position: relative;\n overflow: hidden;\n border: 1px solid black;\n-\n }\n\\ No newline at end of file\ndiff --git a/src/snek/static/html_frame.js b/src/snek/static/html-frame.js\nsimilarity index 62%\nrename from src/snek/static/html_frame.js\nrename to src/snek/static/html-frame.js\nindex 19a0c34..22581ce 100644\n--- a/src/snek/static/html_frame.js\n+++ b/src/snek/static/html-frame.js\n@@ -12,28 +12,25 @@ class HTMLFrame extends HTMLElement {\n if (url) {\n const fullUrl = url.startsWith(\"/\") ? window.location.origin + url : new URL(window.location.origin + \"/http-get\")\n if(!url.startsWith(\"/\"))\n- fullUrl.searchParams.set('url', url)\n- console.info(fullUrl) \n- this.fetchAndDisplayHtml(fullUrl.toString());\n+ fullUrl.searchParams.set('url', url) \n+ this.loadAndRender(fullUrl.toString());\n } else {\n- this.container.textContent = \"No URL provided!\";\n+ this.container.textContent = \"No source URL!\";\n }\n }\n \n- async fetchAndDisplayHtml(url) {\n+ async loadAndRender(url) {\n try {\n const response = await fetch(url);\n if (!response.ok) {\n- throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);\n+ throw new Error(`Error: ${response.status} ${response.statusText}`);\n }\n const html = await response.text();\n+ this.container.innerHTML = html;\n \n } catch (error) {\n this.container.textContent = `Error: ${error.message}`;\n }\n }\n }\n-\n customElements.define('html-frame', HTMLFrame);\n\\ No newline at end of file\ndiff --git a/src/snek/static/register.css b/src/snek/static/register__.css\nsimilarity index 74%\nrename from src/snek/static/register.css\nrename to src/snek/static/register__.css\nindex fc4ca0f..57186b8 100644\n--- a/src/snek/static/register.css\n+++ b/src/snek/static/register__.css\n@@ -1,24 +1,12 @@\n+\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n- body {\n- font-family: Arial, sans-serif;\n- line-height: 1.5;\n- display: flex;\n- flex-direction: column;\n- justify-content: center;\n- align-items: center;\n- height: 100vh;\n- }\n \n+ \n .registration-container {\n border-radius: 10px;\n@@ -26,16 +14,15 @@\n width: 400px;\n box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n text-align: center;\n+ left: calc(50%-200);\n }\n \n .registration-container h1 {\n font-size: 2em;\n margin-bottom: 20px;\n }\n \n .registration-container input {\n width: 100%;\n padding: 10px;\n@@ -47,7 +34,6 @@\n font-size: 1em;\n }\n \n .registration-container button {\n width: 100%;\n padding: 10px;\n@@ -65,7 +51,6 @@\n }\n \n .registration-container a {\n text-decoration: none;\n@@ -79,14 +64,11 @@\n }\n \n .error {\n font-size: 0.9em;\n margin-top: 5px;\n }\n- \n @media (max-width: 500px) {\n .registration-container {\n width: 90%;\ndiff --git a/src/snek/system/__init__.py b/src/snek/system/__init__.py\nnew file mode 100644\nindex 0000000..e69de29\ndiff --git a/src/snek/system/form.py b/src/snek/system/form.py\nnew file mode 100644\nindex 0000000..68f7c0f\n--- /dev/null\n+++ b/src/snek/system/form.py\n@@ -0,0 +1,171 @@\n+from snek.system import model \n+\n+class HTMLElement(model.ModelField):\n+ def __init__(self,id:str=None, tag:str=\"div\", name:str=None,html:str=None, class_name:str=None, text:str=None, *args, **kwargs):\n+ \"\"\"\n+ Create a new HTMLElement.\n+ \n+ :param id: The id of the element\n+ :param tag: The tag of the element\n+ :param name: The name of the element, used to generate a class name if not provided\n+ :param html: The inner html of the element\n+ :param class_name: The class name of the element\n+ :param text: The text of the element\n+ \"\"\"\n+ self.tag = tag\n+ self.text = text\n+ self.id = id \n+ self.class_name = class_name or name\n+ self.html = html \n+ super().__init__(name=name,*args, **kwargs)\n+\n+ def to_json(self):\n+ \"\"\"\n+ Return a json representation of the element.\n+ \n+ This will return a dict with the following keys:\n+ \n+ - text: The text of the element\n+ - id: The id of the element\n+ - html: The inner html of the element\n+ - class_name: The class name of the element\n+ - tag: The tag of the element\n+ \n+ :return: A json representation of the element\n+ :rtype: dict\n+ \"\"\"\n+ result = super().to_json()\n+ result['text'] = self.text \n+ result['id'] = self.id \n+ result['html'] = self.html \n+ result['class_name'] = self.class_name\n+ result['tag'] = self.tag\n+ return result \n+\n+class FormElement(HTMLElement):\n+ pass\n+ \n+class FormInputElement(FormElement):\n+\n+ def __init__(self,type=\"text\",place_holder=None, *args, **kwargs):\n+ \"\"\"\n+ Initialize a FormInputElement with specified attributes.\n+\n+ :param type: The type of the input element (default is \"text\").\n+ :param place_holder: The placeholder text for the input element.\n+ :param args: Additional positional arguments.\n+ :param kwargs: Additional keyword arguments.\n+ \"\"\"\n+\n+ super().__init__(tag=\"input\", *args, **kwargs)\n+ self.place_holder = place_holder \n+ self.type = type\n+ \n+\n+ def to_json(self):\n+ \"\"\"\n+ Return a json representation of the element.\n+\n+ This will return a dict with the following keys:\n+\n+ - place_holder: The placeholder text for the input element\n+ - type: The type of the input element\n+\n+ :return: A json representation of the element\n+ :rtype: dict\n+ \"\"\"\n+ data = super().to_json()\n+ data[\"place_holder\"] = self.place_holder\n+ data[\"type\"] = self.type\n+ return data \n+ \n+class FormButtonElement(FormElement):\n+ def __init__(self, tag=\"button\", *args, **kwargs):\n+ \"\"\"\n+ Initialize a FormButtonElement with specified attributes.\n+\n+ :param tag: The tag of the button element (default is \"button\").\n+ :param args: Additional positional arguments.\n+ :param kwargs: Additional keyword arguments.\n+ \"\"\"\n+ super().__init__(tag=tag, *args, **kwargs)\n+\n+\n+class Form(model.BaseModel):\n+ \n+ @property\n+ def html_elements(self):\n+ \"\"\"\n+ Return a list of all :class:`HTMLElement` objects in the form.\n+\n+ This is a convenience property that filters the :attr:`fields` list to only\n+ include elements that are instances of :class:`HTMLElement`.\n+\n+ :return: A list of :class:`HTMLElement` objects\n+ :rtype: list\n+ \"\"\"\n+ json_elements = super().to_json()\n+ return [element for element in self.fields if isinstance(element,HTMLElement)]\n+ def set_user_data(self, data):\n+ \"\"\"\n+ Set user data for the form by updating the fields with the provided data.\n+\n+ This method extracts the 'fields' key from the provided data dictionary\n+ and passes it to the parent class's `set_user_data` method to update the\n+ form fields accordingly.\n+\n+ :param data: A dictionary containing the form data, expected to have a \n+ 'fields' key with the data to update the form fields.\n+ \"\"\"\n+\n+ return super().set_user_data(data.get('fields'))\n+\n+ def to_json(self, encode=False):\n+ \"\"\"\n+ Return a JSON representation of the form, including field values and metadata.\n+\n+ This method returns a dictionary with the following keys:\n+\n+ - ``fields``: A dictionary of field names to their current values.\n+ - ``is_valid``: A boolean indicating whether the form is valid.\n+ - ``errors``: A dictionary of field names to lists of error strings.\n+\n+ If the ``encode`` argument is ``True``, the dictionary will be JSON-encoded\n+ before being returned. Otherwise, the dictionary is returned directly.\n+\n+ :param encode: If ``True``, JSON-encode the returned dictionary.\n+ :type encode: bool\n+ :return: A JSON representation of the form.\n+ :rtype: dict\n+ \"\"\"\n+ elements = super().to_json()\n+ html_elements = {}\n+ for element in elements.keys():\n+ print(\"DDD!\",element,flush=True)\n+ field = getattr(self,element)\n+ if isinstance(field,HTMLElement):\n+ print(\"QQQQ!\",element,flush=True)\n+ try:\n+ html_elements[element] = elements[element]\n+ except KeyError:\n+ pass \n+\n+ return dict(fields=html_elements,is_valid=self.is_valid,errors=self.errors)\n+ @property\n+ def errors(self):\n+ \"\"\"\n+ Return a list of all error strings from all fields in the form.\n+\n+ The list will be empty if all fields are valid.\n+\n+ :return: A list of error strings.\n+ :rtype: list\n+ \"\"\"\n+ result = []\n+ for field in self.html_elements:\n+ result += field.errors \n+ return result \n+ @property\n+ def is_valid(self):\n+ return all(element.is_valid for element in self.html_elements)\ndiff --git a/src/snek/http.py b/src/snek/system/http.py\nsimilarity index 100%\nrename from src/snek/http.py\nrename to src/snek/system/http.py\ndiff --git a/src/snek/middleware.py b/src/snek/system/middleware.py\nsimilarity index 100%\nrename from src/snek/middleware.py\nrename to src/snek/system/middleware.py\ndiff --git a/src/snek/models.py b/src/snek/system/model.py\nsimilarity index 78%\nrename from src/snek/models.py\nrename to src/snek/system/model.py\nindex ec5d9ce..a699a9e 100644\n--- a/src/snek/models.py\n+++ b/src/snek/system/model.py\n@@ -23,7 +23,7 @@ def validate_attrs(required=False,min_length=None,max_length=None,regex=None,**k\n return add_attrs(required=required,min_length=min_length,max_length=max_length,regex=regex,**kwargs)(func)\n \n class Validator:\n-\n+ _index = 0\n @property\n def value(self):\n return self._value \n@@ -34,12 +34,14 @@ class Validator:\n \n @property\n def initial_value(self):\n- return None\n+ return self.value\n \n def custom_validation(self):\n return True\n \n def __init__(self,required=False,min_num=None,max_num=None,min_length=None,max_length=None,regex=None,value=None,kind=None,help_text=None,**kwargs):\n+ self.index = Validator._index\n+ Validator._index += 1\n self.required = required \n self.min_num = min_num \n self.max_num = max_num\n@@ -47,8 +49,10 @@ class Validator:\n self.max_length = max_length \n self.regex = regex \n self._value = None \n- self.value = value \n- self.type = kind\n+ self.value = value\n+ print(\"xxxx\", value,flush=True) \n+ \n+ self.kind = kind\n self.help_text = help_text \n self.__dict__.update(kwargs)\n @property \n@@ -61,7 +65,7 @@ class Validator:\n if self.value is None:\n return error_list \n \n- if self.type == float or self.type == int:\n+ if self.kind == float or self.kind == int:\n if self.min_num is not None and self.value < self.min_num:\n error_list.append(\"Field should be minimal {}.\".format(self.min_num))\n if self.max_num is not None and self.value > self.max_num:\n@@ -70,10 +74,11 @@ class Validator:\n error_list.append(\"Field should be minimal {} characters long.\".format(self.min_length))\n if self.max_length is not None and len(self.value) > self.max_length:\n error_list.append(\"Field should be maximal {} characters long.\".format(self.max_length))\n+ print(self.regex, self.value,flush=True)\n if not self.regex is None and not self.value is None and not re.match(self.regex, self.value):\n error_list.append(\"Invalid value.\".format(self.regex))\n- if not self.type is None and type(self.value) != self.type:\n- error_list.append(\"Invalid type. It is supposed to be {}.\".format(self.type))\n+ if not self.kind is None and type(self.value) != self.kind:\n+ error_list.append(\"Invalid kind. It is supposed to be {}.\".format(self.kind))\n return error_list \n \n def validate(self):\n@@ -89,6 +94,8 @@ class Validator:\n except ValueError:\n return False\n \n+ \n+\n def to_json(self):\n return {\n \"required\": self.required,\n@@ -98,19 +105,26 @@ class Validator:\n \"max_length\": self.max_length,\n \"regex\": self.regex,\n \"value\": self.value,\n- \"type\": self.type,\n+ \"kind\": str(self.kind),\n \"help_text\": self.help_text,\n \"errors\": self.errors,\n- \"is_valid\": self.is_valid\n+ \"is_valid\": self.is_valid,\n+ \"index\":self.index\n }\n \n class ModelField(Validator):\n+\n+ index = 1\n def __init__(self,name=None,save=True, *args, **kwargs):\n self.name = name \n- \n self.save = save\n super().__init__(*args, **kwargs)\n \n+ def to_json(self):\n+ result = super().to_json()\n+ result['name'] = self.name\n+ return result \n+\n \n class CreatedField(ModelField):\n \n@@ -146,9 +160,11 @@ class BaseModel:\n updated_at = UpdatedField(name=\"updated_at\",regex=TIMESTAMP_REGEX,place_holder=\"Updated at\")\n deleted_at = DeletedField(name=\"deleted_at\",regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\")\n \n+ \n def __init__(self, *args, **kwargs):\n print(self.__dict__)\n print(dir(self.__class__))\n+ self.fields = {}\n for key in dir(self.__class__):\n obj = getattr(self.__class__,key)\n \n@@ -156,6 +172,7 @@ class BaseModel:\n self.__dict__[key] = copy.deepcopy(obj)\n print(\"JAAA\")\n self.__dict__[key].value = kwargs.pop(key,self.__dict__[key].initial_value)\n+ self.fields[key] = self.__dict__[key]\n \n def __setitem__(self, key, value):\n obj = self.__dict__.get(key)\n@@ -169,6 +186,22 @@ class BaseModel:\n return obj.value \n return obj\n \n+ def set_user_data(self, data):\n+ for key, value in data.items():\n+ field = self.fields.get(key)\n+ if not field:\n+ continue \n+ if value.get('name'):\n+ value = value.get('value')\n+ field.value = value\n+ \n+\n+ @property \n+ def is_valid(self):\n+ for field in self.fields.values():\n+ if not field.is_valid:\n+ return False\n+ return True\n \n def __getitem__(self, key):\n obj = self.__dict__.get(key)\n@@ -180,7 +213,7 @@ class BaseModel:\n if isinstance(obj,Validator):\n obj.value = value\n else:\n- setattr(self,key,value)\n@@ -201,6 +234,7 @@ class BaseModel:\n \"updated_at\": self.updated_at.value,\n \"deleted_at\": self.deleted_at.value\n })\n+ \n for key,value in self.__dict__.items(): \n if key == \"record\":\n continue\n@@ -225,39 +259,8 @@ class FormElement(ModelField):\n self.place_holder = place_holder \n super().__init__(*args, **kwargs)\n \n-\n def to_json(self):\n data = super().to_json()\n data[\"name\"] = self.name \n data[\"place_holder\"] = self.place_holder\n return data \n-\n-\n-\n-\n-class TestModel(BaseModel):\n-\n- first_name = FormElement(name=\"first_name\",required=True,min_length=3,max_length=20,regex=r\"^[a-zA-Z0-9_]+$\",place_holder=\"First name\")\n- last_name = FormElement(name=\"last_name\",required=True,min_length=3,max_length=20,regex=r\"^[a-zA-Z0-9_]+$\",place_holder=\"Last name\")\n- email = FormElement(name=\"email\",required=True,regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",place_holder=\"Email address\") \n-\n-class Form:\n- username = FormElement(required=True,min_length=3,max_length=20,regex=r\"^[a-zA-Z0-9_]+$\",place_holder=\"Username\")\n- email = FormElement(required=True,regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",place_holder=\"Email address\")\n- def __init__(self, *args, **kwargs):\n- self.place_holder = kwargs.pop(\"place_holder\",None) \n-\n-\n-if __name__ == \"__main__\":\n- model = TestModel(first_name=\"John\",last_name=\"Doe\",email=\"n9K9p@example.com\",password=\"Password123\")\n- model2 = TestModel(first_name=\"John\",last_name=\"Doe\",email=\"ddd\",password=\"zzz\")\n- model.first_name = \"AAA\"\n- print(model.first_name)\n- print(model.first_name.value)\n- \n- print(model.first_name)\n- print(model.first_name.value)\n- print(model.to_json(True))\n- print(model2.to_json(True))\n- print(model2.record)\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nnew file mode 100644\nindex 0000000..5b1bdf2\n--- /dev/null\n+++ b/src/snek/templates/base.html\n@@ -0,0 +1,31 @@\n+\n+\n+\n+ \n+ \n+ {% block title %}{% endblock %}\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+\n+\n+
\n+ {% block header %}\n+ {% endblock %}\n+\n+
\n+
\n+ \n+ {% block main %}\n+ {% endblock %}\n+
\n+\n+\n\\ No newline at end of file\ndiff --git a/src/snek/templates/base_chat.html b/src/snek/templates/base_chat.html\nnew file mode 100644\nindex 0000000..09c025b\n--- /dev/null\n+++ b/src/snek/templates/base_chat.html\n@@ -0,0 +1,31 @@\n+\n+\n+\n+ \n+ \n+ {% block title %}{% endblock %}\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+\n+\n+
\n+ {% block header %}\n+ {% endblock %}\n+\n+
\n+
\n+ \n+ {% block main %}\n+ {% endblock %}\n+
\n+\n+\n\\ No newline at end of file\ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nnew file mode 100644\nindex 0000000..1ee1f77\n--- /dev/null\n+++ b/src/snek/templates/index.html\n@@ -0,0 +1,20 @@\n+\n+\n+\n+ \n+ \n+ Snek chat by Molodetz\n+ \n+ \n+ \n+\n+\n+
\n+

Snek

\n+ \n+ Or\n+ \n+\n+
\n+\n+\ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex d37f3fa..c09ec70 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -1,20 +1,5 @@\n-\n-\n-\n- \n- \n- Register\n- \n-\n-\n-
\n-

Login

\n-
\n- \n- \n- \n- Not having an account yet? Register here.\n-
\n-
\n-\n-\n+{% extends \"base.html\" %}\n+\n+{% block main %}\n+ \n+{% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex da41629..61da961 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -1,22 +1,5 @@\n-\n-\n-\n- \n- \n- Register\n- \n-\n-\n-
\n-

Register

\n-
\n- \n- \n- \n- \n- \n-
\n-
\n-\n-\n+{% extends \"base.html\" %}\n+\n+{% block main %}\n+ \n+{% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/templates/test.html b/src/snek/templates/web.html\nsimilarity index 92%\nrename from src/snek/templates/test.html\nrename to src/snek/templates/web.html\nindex c23103a..0403e1b 100644\n--- a/src/snek/templates/test.html\n+++ b/src/snek/templates/web.html\n@@ -4,9 +4,7 @@\n \n \n Dark Themed Chat Application\n- \n- \n- \n+ \n \n \n
\n@@ -59,5 +57,4 @@\n \n \n \n-\n-\n+\n\\ No newline at end of file\ndiff --git a/src/snek/view/base.py b/src/snek/view/base.py\nnew file mode 100644\nindex 0000000..d962ee5\n--- /dev/null\n+++ b/src/snek/view/base.py\n@@ -0,0 +1,31 @@\n+from aiohttp import web \n+\n+class BaseView(web.View):\n+ \n+ @property \n+ def app(self):\n+ return self.request.app\n+ \n+ @property\n+ def db(self):\n+ return self.app.db\n+\n+ def json_response(self, data):\n+ return web.json_response(data)\n+\n+ def render_template(self, template_name, context=None):\n+ return self.request.app.render_template(template_name, self.request,context)\n+ \n+class BaseFormView(BaseView):\n+\n+ form = None \n+\n+ async def get(self):\n+ form = self.form()\n+ return self.json_response(form.to_json())\n+ \n+ async def post(self):\n+ form = self.form()\n+ post = await self.request.json()\n+ form.set_user_data(post['form'])\n+ return self.json_response(form.to_json()) \n\\ No newline at end of file\ndiff --git a/src/snek/view/index.py b/src/snek/view/index.py\nnew file mode 100644\nindex 0000000..a5d8b92\n--- /dev/null\n+++ b/src/snek/view/index.py\n@@ -0,0 +1,6 @@\n+from snek.view.base import BaseView\n+\n+class IndexView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"index.html\")\ndiff --git a/src/snek/view/login.py b/src/snek/view/login.py\nnew file mode 100644\nindex 0000000..3a3beaf\n--- /dev/null\n+++ b/src/snek/view/login.py\n@@ -0,0 +1,13 @@\n+from snek.form.register import RegisterForm\n+from snek.view.base import BaseView \n+\n+class LoginView(BaseView):\n+\n+ async def get(self):\n+ \n+ async def post(self):\n+ form = RegisterForm()\n+ form.set_user_data(await self.request.post())\n+ print(form.is_valid())\ndiff --git a/src/snek/view/login_form.py b/src/snek/view/login_form.py\nnew file mode 100644\nindex 0000000..26527da\n--- /dev/null\n+++ b/src/snek/view/login_form.py\n@@ -0,0 +1,5 @@\n+from snek.view.base import BaseFormView\n+from snek.form.login import LoginForm\n+\n+class LoginFormView(BaseFormView):\n+ form = LoginForm\n\\ No newline at end of file\ndiff --git a/src/snek/view/register.py b/src/snek/view/register.py\nnew file mode 100644\nindex 0000000..095b7a3\n--- /dev/null\n+++ b/src/snek/view/register.py\n@@ -0,0 +1,6 @@\n+from snek.view.base import BaseView \n+\n+class RegisterView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"register.html\") \n\\ No newline at end of file\ndiff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py\nnew file mode 100644\nindex 0000000..0ae7630\n--- /dev/null\n+++ b/src/snek/view/register_form.py\n@@ -0,0 +1,5 @@\n+from snek.form.register import RegisterForm\n+from snek.view.base import BaseFormView \n+\n+class RegisterFormView(BaseFormView):\n+ form = RegisterForm\n\\ No newline at end of file\ndiff --git a/src/snek/view/view.py b/src/snek/view/view.py\nnew file mode 100644\nindex 0000000..ea642a3\n--- /dev/null\n+++ b/src/snek/view/view.py\n@@ -0,0 +1,6 @@\n+from snek.view.base import BaseView \n+\n+class WebView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"web.html\")\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor system and views for improved structure and maintainability", "commit": "d20079f3ed8f261bcda0f5379f4c9e23ee941527", "diff": "commit d20079f3ed8f261bcda0f5379f4c9e23ee941527\nAuthor: retoor \nDate: Fri Jan 24 14:00:10 2025 +0100\n\n Complete system.\n\ndiff --git a/.gitignore b/.gitignore\nindex 3747073..ece77be 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -1,5 +1,8 @@\n .vscode\n .history\n+.resources\n+.backup*\n+docs\n *.db*\n *.png\ndiff --git a/Dockerfile b/Dockerfile\nindex 47c0ece..9af8e87 100644\n--- a/Dockerfile\n+++ b/Dockerfile\n@@ -1,5 +1,5 @@\n FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf \n-FROM python:3.10-alpine\n+FROM python:3.12.8-alpine3.21\n WORKDIR /code\n ENV FLASK_APP=app.py\n ENV FLASK_RUN_HOST=0.0.0.0\n@@ -37,5 +37,5 @@ RUN pip install --upgrade pip\n RUN pip install -e .\n EXPOSE 8081\n \n-python -m snek.app\n+CMD [\"python\",\"-m\",\"snek.app\"]\ndiff --git a/pyproject.toml b/pyproject.toml\nindex d98557e..cc36846 100644\n--- a/pyproject.toml\n+++ b/pyproject.toml\n@@ -20,6 +20,8 @@ dependencies = [\n \"beautifulsoup4\",\n \"gunicorn\",\n \"imgkit\",\n- \"wkhtmltopdf\"\n+ \"wkhtmltopdf\",\n+ \"jinja-markdown2\",\n+ \"mistune\"\n ]\n \ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 0e2ed63..deac5d3 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -3,14 +3,16 @@ import pathlib\n from aiohttp import web\n from app.app import Application as BaseApplication\n \n+from jinja_markdown2 import MarkdownExtension\n from snek.system import http\n from snek.system.middleware import cors_middleware\n+from snek.view.about import AboutHTMLView, AboutMDView\n from snek.view.index import IndexView\n from snek.view.login import LoginView\n from snek.view.login_form import LoginFormView\n from snek.view.register import RegisterView\n from snek.view.register_form import RegisterFormView\n-from snek.view.view import WebView\n+from snek.view.web import WebView\n \n \n class Application(BaseApplication):\n@@ -24,6 +26,7 @@ class Application(BaseApplication):\n super().__init__(\n middlewares=middlewares, template_path=self.template_path, *args, **kwargs\n )\n+ self.jinja2_env.add_extension(MarkdownExtension)\n self.setup_router()\n \n def setup_router(self):\n@@ -34,12 +37,14 @@ class Application(BaseApplication):\n name=\"static\",\n show_index=True,\n )\n- self.router.add_view(\"/web\", WebView)\n- self.router.add_view(\"/login\", LoginView)\n- self.router.add_view(\"/login-form\", LoginFormView)\n- self.router.add_view(\"/register\", RegisterView)\n+ self.router.add_view(\"/about.html\", AboutHTMLView)\n+ self.router.add_view(\"/about.md\", AboutMDView)\n+ self.router.add_view(\"/web.html\", WebView)\n+ self.router.add_view(\"/login.html\", LoginView)\n+ self.router.add_view(\"/login-form.json\", LoginFormView)\n+ self.router.add_view(\"/register.html\", RegisterView)\n \n- self.router.add_view(\"/register-form\", RegisterFormView)\n+ self.router.add_view(\"/register-form.json\", RegisterFormView)\n self.router.add_get(\"/http-get\", self.handle_http_get)\n self.router.add_get(\"/http-photo\", self.handle_http_photo)\n \ndiff --git a/src/snek/form/register.py b/src/snek/form/register.py\nindex 60399fb..7dff3e4 100644\n--- a/src/snek/form/register.py\n+++ b/src/snek/form/register.py\n@@ -15,7 +15,7 @@ class RegisterForm(Form):\n )\n email = FormInputElement(\n name=\"email\",\n- required=True,\n+ required=False,\n regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n place_holder=\"Email address\",\n type=\"email\"\ndiff --git a/src/snek/mapper/__init__.py b/src/snek/mapper/__init__.py\nnew file mode 100644\nindex 0000000..dc9e047\n--- /dev/null\n+++ b/src/snek/mapper/__init__.py\n@@ -0,0 +1,12 @@\n+import functools \n+from snek.mapper.user import UserMapper\n+\n+@functools.cache \n+def get_mappers(app=None):\n+ return dict(\n+ user=UserMapper(app=app)\n+\n+ )\n+\n+def get_mapper(name, app=None):\n+ return get_mappers(app=app)[name]\n\\ No newline at end of file\ndiff --git a/src/snek/mapper/user.py b/src/snek/mapper/user.py\nnew file mode 100644\nindex 0000000..5b8671e\n--- /dev/null\n+++ b/src/snek/mapper/user.py\n@@ -0,0 +1,6 @@\n+from snek.system.mapper import BaseMapper\n+from snek.model.user import UserModel\n+\n+class UserMapper(BaseMapper):\n+ table_name = \"user\"\n+ model: UserModel \n\\ No newline at end of file\ndiff --git a/src/snek/model/__init__.py b/src/snek/model/__init__.py\nindex e69de29..52af21a 100644\n--- a/src/snek/model/__init__.py\n+++ b/src/snek/model/__init__.py\n@@ -0,0 +1,12 @@\n+from snek.model.user import UserModel \n+import functools \n+\n+@functools.cache\n+def get_models():\n+ return dict(\n+ user=UserModel\n+\n+ )\n+\n+def get_model(name):\n+ return get_models()[name]\ndiff --git a/src/snek/model/user.py b/src/snek/model/user.py\nindex 44553f8..254b6c9 100644\n--- a/src/snek/model/user.py\n+++ b/src/snek/model/user.py\n@@ -1,6 +1,6 @@\n from snek.system.model import BaseModel,ModelField\n \n-class User(BaseModel):\n+class UserModel(BaseModel):\n \n username = ModelField(\n name=\"username\", \n@@ -11,7 +11,7 @@ class User(BaseModel):\n )\n email = ModelField(\n name=\"email\",\n- required=True,\n+ required=False,\n regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\"\n )\n password = ModelField(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\")\ndiff --git a/src/snek/service/__init__.py b/src/snek/service/__init__.py\nnew file mode 100644\nindex 0000000..4038f70\n--- /dev/null\n+++ b/src/snek/service/__init__.py\n@@ -0,0 +1,12 @@\n+from snek.service.user import UserService \n+import functools \n+\n+@functools.cache\n+def get_services(app):\n+\n+ return dict(\n+ user = UserService(app=app)\n+\n+ )\n+def get_service(name, app=None):\n+ return get_services(app=app)[name]\n\\ No newline at end of file\ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nnew file mode 100644\nindex 0000000..cde4b8c\n--- /dev/null\n+++ b/src/snek/service/user.py\n@@ -0,0 +1,16 @@\n+from snek.system.service import BaseService \n+from snek.system import security \n+\n+class UserService:\n+ mapper_name = \"user\"\n+\n+ async def create_user(self, username, password):\n+ if await self.exists(username=username):\n+ raise Exception(\"User already exists.\")\n+ model = await self.new()\n+ model.username = username\n+ model.password = await security.hash(password)\n+ if await self.save(model):\n+ return model \n+ raise Exception(f\"Failed to create user: {model.errors}.\")\n+ \n\\ No newline at end of file\ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nindex 11fea47..58a67e2 100644\n--- a/src/snek/static/generic-form.js\n+++ b/src/snek/static/generic-form.js\n@@ -224,7 +224,11 @@ class GenericForm extends HTMLElement {\n \n }\n @media (max-width: 500px) {\n+ width:100%;\n+ height:100%;\n form {\n+ height:100%;\n+ width: 100%;\n width: 80%;\n }\n }`\ndiff --git a/src/snek/static/html-frame.js b/src/snek/static/html-frame.js\nindex 22581ce..0d5d4c9 100644\n--- a/src/snek/static/html-frame.js\n+++ b/src/snek/static/html-frame.js\n@@ -26,7 +26,16 @@ class HTMLFrame extends HTMLElement {\n throw new Error(`Error: ${response.status} ${response.statusText}`);\n }\n const html = await response.text();\n- this.container.innerHTML = html;\n+ if(url.endsWith(\".md\")){\n+ const parent = this\n+ const markdownElement = document.createElement('div')\n+ markdownElement.innerHTML = html\n+ document.body.appendChild(markdownElement)\n+ \n+ }else{\n+ this.container.innerHTML = html;\n+ }\n \n } catch (error) {\n this.container.textContent = `Error: ${error.message}`;\ndiff --git a/src/snek/static/markdown-frame.js b/src/snek/static/markdown-frame.js\nnew file mode 100644\nindex 0000000..e2b7a77\n--- /dev/null\n+++ b/src/snek/static/markdown-frame.js\n@@ -0,0 +1,39 @@\n+\n+\n+\n+class HTMLFrame extends HTMLElement {\n+ constructor() {\n+ super();\n+ this.attachShadow({ mode: 'open' });\n+ this.container = document.createElement('div');\n+ this.shadowRoot.appendChild(this.container);\n+ }\n+\n+ connectedCallback() {\n+ this.container.classList.add(\"html_frame\")\n+ const url = this.getAttribute('url');\n+ if (url) {\n+ const fullUrl = url.startsWith(\"/\") ? window.location.origin + url : new URL(window.location.origin + \"/http-get\")\n+ if(!url.startsWith(\"/\"))\n+ fullUrl.searchParams.set('url', url) \n+ this.loadAndRender(fullUrl.toString());\n+ } else {\n+ this.container.textContent = \"No source URL!\";\n+ }\n+ }\n+\n+ async loadAndRender(url) {\n+ try {\n+ const response = await fetch(url);\n+ if (!response.ok) {\n+ throw new Error(`Error: ${response.status} ${response.statusText}`);\n+ }\n+ const html = await response.text();\n+ this.container.innerHTML = html;\n+ \n+ } catch (error) {\n+ this.container.textContent = `Error: ${error.message}`;\n+ }\n+ }\n+ }\n+ customElements.define('markdown-frame', HTMLFrame);\n\\ No newline at end of file\ndiff --git a/src/snek/static/style.css b/src/snek/static/style.css\nnew file mode 100644\nindex 0000000..990fcf9\n--- /dev/null\n+++ b/src/snek/static/style.css\n@@ -0,0 +1,20 @@\n+h1 {\n+ font-size: 2em;\n+ margin-bottom: 20px;\n+}\n+\n+h2 {\n+ font-size: 1.4em;\n+ margin-bottom: 20px;\n+}\n+body {\n+\n+}\n+div {\n+ text-align: left;\n+\n+}\n\\ No newline at end of file\ndiff --git a/src/snek/system/api.py b/src/snek/system/api.py\nnew file mode 100644\nindex 0000000..e69de29\ndiff --git a/src/snek/system/form.py b/src/snek/system/form.py\nindex 68f7c0f..f9ebebb 100644\n--- a/src/snek/system/form.py\n+++ b/src/snek/system/form.py\n@@ -1,171 +1,96 @@\n-from snek.system import model \n+\n+\n+\n+\n+from snek.system import model\n \n class HTMLElement(model.ModelField):\n- def __init__(self,id:str=None, tag:str=\"div\", name:str=None,html:str=None, class_name:str=None, text:str=None, *args, **kwargs):\n- \"\"\"\n- Create a new HTMLElement.\n- \n- :param id: The id of the element\n- :param tag: The tag of the element\n- :param name: The name of the element, used to generate a class name if not provided\n- :param html: The inner html of the element\n- :param class_name: The class name of the element\n- :param text: The text of the element\n- \"\"\"\n+ def __init__(self, id=None, tag=\"div\", name=None, html=None, class_name=None, text=None, *args, **kwargs):\n self.tag = tag\n self.text = text\n- self.id = id \n+ self.id = id\n self.class_name = class_name or name\n- self.html = html \n- super().__init__(name=name,*args, **kwargs)\n+ self.html = html\n+ super().__init__(name=name, *args, **kwargs)\n \n def to_json(self):\n- \"\"\"\n- Return a json representation of the element.\n- \n- This will return a dict with the following keys:\n- \n- - text: The text of the element\n- - id: The id of the element\n- - html: The inner html of the element\n- - class_name: The class name of the element\n- - tag: The tag of the element\n- \n- :return: A json representation of the element\n- :rtype: dict\n- \"\"\"\n result = super().to_json()\n- result['text'] = self.text \n- result['id'] = self.id \n- result['html'] = self.html \n+ result['text'] = self.text\n+ result['id'] = self.id\n+ result['html'] = self.html\n result['class_name'] = self.class_name\n result['tag'] = self.tag\n- return result \n+ return result\n \n class FormElement(HTMLElement):\n pass\n- \n-class FormInputElement(FormElement):\n-\n- def __init__(self,type=\"text\",place_holder=None, *args, **kwargs):\n- \"\"\"\n- Initialize a FormInputElement with specified attributes.\n-\n- :param type: The type of the input element (default is \"text\").\n- :param place_holder: The placeholder text for the input element.\n- :param args: Additional positional arguments.\n- :param kwargs: Additional keyword arguments.\n- \"\"\"\n \n+class FormInputElement(FormElement):\n+ def __init__(self, type=\"text\", place_holder=None, *args, **kwargs):\n super().__init__(tag=\"input\", *args, **kwargs)\n- self.place_holder = place_holder \n+ self.place_holder = place_holder\n self.type = type\n- \n \n def to_json(self):\n- \"\"\"\n- Return a json representation of the element.\n-\n- This will return a dict with the following keys:\n-\n- - place_holder: The placeholder text for the input element\n- - type: The type of the input element\n-\n- :return: A json representation of the element\n- :rtype: dict\n- \"\"\"\n data = super().to_json()\n data[\"place_holder\"] = self.place_holder\n data[\"type\"] = self.type\n- return data \n- \n+ return data\n+\n class FormButtonElement(FormElement):\n def __init__(self, tag=\"button\", *args, **kwargs):\n- \"\"\"\n- Initialize a FormButtonElement with specified attributes.\n-\n- :param tag: The tag of the button element (default is \"button\").\n- :param args: Additional positional arguments.\n- :param kwargs: Additional keyword arguments.\n- \"\"\"\n super().__init__(tag=tag, *args, **kwargs)\n \n-\n class Form(model.BaseModel):\n- \n @property\n def html_elements(self):\n- \"\"\"\n- Return a list of all :class:`HTMLElement` objects in the form.\n-\n- This is a convenience property that filters the :attr:`fields` list to only\n- include elements that are instances of :class:`HTMLElement`.\n-\n- :return: A list of :class:`HTMLElement` objects\n- :rtype: list\n- \"\"\"\n json_elements = super().to_json()\n- return [element for element in self.fields if isinstance(element,HTMLElement)]\n- def set_user_data(self, data):\n- \"\"\"\n- Set user data for the form by updating the fields with the provided data.\n-\n- This method extracts the 'fields' key from the provided data dictionary\n- and passes it to the parent class's `set_user_data` method to update the\n- form fields accordingly.\n-\n- :param data: A dictionary containing the form data, expected to have a \n- 'fields' key with the data to update the form fields.\n- \"\"\"\n+ return [element for element in self.fields if isinstance(element, HTMLElement)]\n \n+ def set_user_data(self, data):\n return super().set_user_data(data.get('fields'))\n \n def to_json(self, encode=False):\n- \"\"\"\n- Return a JSON representation of the form, including field values and metadata.\n-\n- This method returns a dictionary with the following keys:\n-\n- - ``fields``: A dictionary of field names to their current values.\n- - ``is_valid``: A boolean indicating whether the form is valid.\n- - ``errors``: A dictionary of field names to lists of error strings.\n-\n- If the ``encode`` argument is ``True``, the dictionary will be JSON-encoded\n- before being returned. Otherwise, the dictionary is returned directly.\n-\n- :param encode: If ``True``, JSON-encode the returned dictionary.\n- :type encode: bool\n- :return: A JSON representation of the form.\n- :rtype: dict\n- \"\"\"\n elements = super().to_json()\n html_elements = {}\n for element in elements.keys():\n- print(\"DDD!\",element,flush=True)\n- field = getattr(self,element)\n- if isinstance(field,HTMLElement):\n- print(\"QQQQ!\",element,flush=True)\n+ field = getattr(self, element)\n+ if isinstance(field, HTMLElement):\n try:\n html_elements[element] = elements[element]\n except KeyError:\n- pass \n+ pass\n+ return dict(fields=html_elements, is_valid=self.is_valid, errors=self.errors)\n \n- return dict(fields=html_elements,is_valid=self.is_valid,errors=self.errors)\n @property\n def errors(self):\n- \"\"\"\n- Return a list of all error strings from all fields in the form.\n-\n- The list will be empty if all fields are valid.\n-\n- :return: A list of error strings.\n- :rtype: list\n- \"\"\"\n result = []\n for field in self.html_elements:\n- result += field.errors \n- return result \n+ result += field.errors\n+ return result\n+\n @property\n def is_valid(self):\n- return all(element.is_valid for element in self.html_elements)\n+ return all(element.is_valid for element in self.html_elements)\n\\ No newline at end of file\ndiff --git a/src/snek/system/http.py b/src/snek/system/http.py\nindex 0b16bee..b5e8b4f 100644\n--- a/src/snek/system/http.py\n+++ b/src/snek/system/http.py\n@@ -1,77 +1,99 @@\n-from aiohttp import web \n-import aiohttp \n+\n+\n+\n+\n+\n+from aiohttp import web\n+import aiohttp\n from app.cache import time_cache_async\n from bs4 import BeautifulSoup\n from urllib.parse import urljoin\n-import pathlib \n-import uuid \n-import imgkit \n+import pathlib\n+import uuid\n+import imgkit\n import asyncio\n import zlib\n-import io \n+import io\n \n async def crc32(data):\n try:\n data = data.encode()\n except:\n- pass \n- result = \"crc32\" + str(zlib.crc32(data))\n- return result \n+ pass\n+ return \"crc32\" + str(zlib.crc32(data))\n \n-async def get_file(name,suffix=\".cache\"):\n+async def get_file(name, suffix=\".cache\"):\n name = await crc32(name)\n path = pathlib.Path(\".\").joinpath(\"cache\")\n if not path.exists():\n- path.mkdir(parents=True,exist_ok=True)\n- path = path.joinpath(name + suffix)\n- return path\n-\n-\n+ path.mkdir(parents=True, exist_ok=True)\n+ return path.joinpath(name + suffix)\n \n async def public_touch(name=None):\n- path = pathlib.Path(\".\").joinpath(str(uuid.uuid4())+name)\n+ path = pathlib.Path(\".\").joinpath(str(uuid.uuid4()) + name)\n path.open(\"wb\").close()\n- return path \n+ return path\n \n async def create_site_photo(url):\n loop = asyncio.get_event_loop()\n if not url.startswith(\"https\"):\n- output_path = await get_file(\"site-screenshot-\" + url,\".png\")\n+ output_path = await get_file(\"site-screenshot-\" + url, \".png\")\n \n if output_path.exists():\n return output_path\n output_path.touch()\n+\n def make_photo():\n imgkit.from_url(url, output_path.absolute())\n- return output_path \n+ return output_path\n \n- return await loop.run_in_executor(None,make_photo)\n+ return await loop.run_in_executor(None, make_photo)\n \n async def repair_links(base_url, html_content):\n soup = BeautifulSoup(html_content, \"html.parser\")\n for tag in soup.find_all(['a', 'img', 'link']):\n+ if tag.has_attr('href') and not tag['href'].startswith(\"http\"):\n tag['href'] = urljoin(base_url, tag['href'])\n+ if tag.has_attr('src') and not tag['src'].startswith(\"http\"):\n tag['src'] = urljoin(base_url, tag['src'])\n- print(\"Fixed: \",tag['src'])\n return soup.prettify()\n \n async def is_html_content(content: bytes):\n try:\n content = content.decode(errors='ignore')\n except:\n- pass \n- marks = [' types.Optional[BaseModel]\n+ if uid:\n+ kwargs['uid'] = uid \n+ model = self.new()\n+ record = self.table.find_one(**kwargs)\n+ return self.model_class.from_record(mapper=self,record=record)\n+\n+ async def exists(self, **kwargs):\n+ return self.table.exists(**kwargs)\n+\n+ async def count(self, **kwargs) -> int:\n+ return self.table.count(**kwargs)\n+\n+ async def save(self, model:BaseModel) -> bool:\n+ record = model.record\n+ if not record.get('uid'):\n+ raise Exception(f\"Attempt to save without uid: {record}.\")\n+ return self.table.upsert(record,['uid'])\n+\n+ async def find(self, **kwargs) -> types.List[BaseModel]:\n+ if not kwargs.get(\"_limit\"):\n+ kwargs[\"_limit\"] = self.default_limit\n+ for record in self.table.find(**kwargs):\n+ yield self.model_class.from_record(mapper=self,record=record)\n+ \n+ async def delete(self, kwargs=None)-> int: \n+ if not kwargs or not isinstance(kwargs, dict):\n+ raise Exception(\"Can't execute delete with no filter.\")\n+ return self.table.delete(**kwargs)\ndiff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py\nnew file mode 100644\nindex 0000000..bded949\n--- /dev/null\n+++ b/src/snek/system/markdown.py\n@@ -0,0 +1,43 @@\n+\n+\n+from mistune import escape\n+from mistune import Markdown\n+from mistune import HTMLRenderer\n+from pygments import highlight\n+from pygments.lexers import get_lexer_by_name\n+from pygments.formatters import html\n+from pygments.styles import get_style_by_name\n+\n+\n+class MarkdownRenderer(HTMLRenderer):\n+ def __init__(self, app, template):\n+ self.template = template\n+ \n+ self.app = app\n+ self.env = self.app.jinja2_env\n+ formatter = html.HtmlFormatter()\n+ self.env.globals['highlight_styles'] = formatter.get_style_defs()\n+ def _escape(self,str):\n+ def block_code(self, code, lang=None,info=None):\n+ if not lang:\n+ lang = info\n+ if not lang:\n+ return f\"
{code}
\"\n+ lexer = get_lexer_by_name(lang, stripall=True)\n+ formatter = html.HtmlFormatter(lineseparator=\"
\")\n+ print(code, lang,info, flush=True)\n+ return highlight(code, lexer, formatter)\n+ def render(self):\n+ markdown_string = self.app.template_path.joinpath(self.template).read_text()\n+ renderer = MarkdownRenderer(self.app,self.template)\n+ markdown = Markdown(renderer=renderer)\n+ return markdown(markdown_string)\n+\n+\n+async def render_markdown(app, markdown_string):\n+ renderer = MarkdownRenderer(app,None)\n+ markdown = Markdown(renderer=renderer)\n+ return markdown(markdown_string)\n\\ No newline at end of file\ndiff --git a/src/snek/system/middleware.py b/src/snek/system/middleware.py\nindex 6b801ab..7fe457f 100644\n--- a/src/snek/system/middleware.py\n+++ b/src/snek/system/middleware.py\n@@ -1,4 +1,12 @@\n-from aiohttp import web \n+\n+\n+\n+\n+from aiohttp import web\n \n @web.middleware\n async def no_cors_middleware(request, handler):\n@@ -7,16 +15,15 @@ async def no_cors_middleware(request, handler):\n return response\n \n @web.middleware\n-async def cors_allow_middleware(request ,handler):\n+async def cors_allow_middleware(request, handler):\n response = await handler(request)\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n response.headers[\"Access-Control-Allow-Methods\"] = \"GET, POST, OPTIONS, PUT, DELETE\"\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n- return response \n+ return response\n \n @web.middleware\n async def cors_middleware(request, handler):\n if request.method == \"OPTIONS\":\n response = web.Response()\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n@@ -24,7 +31,6 @@ async def cors_middleware(request, handler):\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n return response\n \n response = await handler(request)\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n response.headers[\"Access-Control-Allow-Methods\"] = \"GET, POST, PUT, DELETE, OPTIONS\"\ndiff --git a/src/snek/system/model.py b/src/snek/system/model.py\nindex a699a9e..eb490b3 100644\n--- a/src/snek/system/model.py\n+++ b/src/snek/system/model.py\n@@ -1,36 +1,67 @@\n+\n+\n+\n+\n+\n import re\n import uuid\n-import json \n-from datetime import datetime , timezone \n+import json\n+from datetime import datetime, timezone\n from collections import OrderedDict\n-import copy \n+import copy\n \n TIMESTAMP_REGEX = r\"^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}\\+\\d{2}:\\d{2}$\"\n \n+\n def now():\n return str(datetime.now(timezone.utc))\n \n+\n def add_attrs(**kwargs):\n def decorator(func):\n for key, value in kwargs.items():\n setattr(func, key, value)\n-\n return func\n return decorator\n \n-def validate_attrs(required=False,min_length=None,max_length=None,regex=None,**kwargs):\n- def decorator(func):\n- return add_attrs(required=required,min_length=min_length,max_length=max_length,regex=regex,**kwargs)(func)\n+\n+def validate_attrs(required=False, min_length=None, max_length=None, regex=None, **kwargs):\n+ def decorator(func):\n+ return add_attrs(required=required, min_length=min_length, max_length=max_length, regex=regex, **kwargs)(func)\n+\n \n class Validator:\n _index = 0\n+\n @property\n def value(self):\n- return self._value \n+ return self._value\n \n- @value.setter \n- def value(self,val):\n- self._value = json.loads(json.dumps(val,default=str))\n+ @value.setter\n+ def value(self, val):\n+ self._value = json.loads(json.dumps(val, default=str))\n \n @property\n def initial_value(self):\n@@ -39,48 +70,49 @@ class Validator:\n def custom_validation(self):\n return True\n \n- def __init__(self,required=False,min_num=None,max_num=None,min_length=None,max_length=None,regex=None,value=None,kind=None,help_text=None,**kwargs):\n+ def __init__(self, required=False, min_num=None, max_num=None, min_length=None, max_length=None, regex=None, value=None, kind=None, help_text=None, **kwargs):\n self.index = Validator._index\n Validator._index += 1\n- self.required = required \n- self.min_num = min_num \n+ self.required = required\n+ self.min_num = min_num\n self.max_num = max_num\n- self.min_length = min_length \n- self.max_length = max_length \n- self.regex = regex \n- self._value = None \n+ self.min_length = min_length\n+ self.max_length = max_length\n+ self.regex = regex\n+ self._value = None\n self.value = value\n- print(\"xxxx\", value,flush=True) \n- \n+ print(\"xxxx\", value, flush=True)\n+\n self.kind = kind\n- self.help_text = help_text \n+ self.help_text = help_text\n self.__dict__.update(kwargs)\n- @property \n+\n+ @property\n def errors(self):\n error_list = []\n if self.value is None and self.required:\n error_list.append(\"Field is required.\")\n- return error_list \n- \n+ return error_list\n+\n if self.value is None:\n- return error_list \n+ return error_list\n \n- if self.kind == float or self.kind == int:\n+ if self.kind in [int, float]:\n if self.min_num is not None and self.value < self.min_num:\n- error_list.append(\"Field should be minimal {}.\".format(self.min_num))\n+ error_list.append(f\"Field should be minimal {self.min_num}.\")\n if self.max_num is not None and self.value > self.max_num:\n- error_list.append(\"Field should be maximal {}.\".format(self.max_num))\n+ error_list.append(f\"Field should be maximal {self.max_num}.\")\n if self.min_length is not None and len(self.value) < self.min_length:\n- error_list.append(\"Field should be minimal {} characters long.\".format(self.min_length))\n+ error_list.append(f\"Field should be minimal {self.min_length} characters long.\")\n if self.max_length is not None and len(self.value) > self.max_length:\n- error_list.append(\"Field should be maximal {} characters long.\".format(self.max_length))\n- print(self.regex, self.value,flush=True)\n- if not self.regex is None and not self.value is None and not re.match(self.regex, self.value):\n- error_list.append(\"Invalid value.\".format(self.regex))\n- if not self.kind is None and type(self.value) != self.kind:\n- error_list.append(\"Invalid kind. It is supposed to be {}.\".format(self.kind))\n- return error_list \n- \n+ error_list.append(f\"Field should be maximal {self.max_length} characters long.\")\n+ print(self.regex, self.value, flush=True)\n+ if self.regex and self.value and not re.match(self.regex, self.value):\n+ error_list.append(\"Invalid value.\")\n+ if self.kind and not isinstance(self.value, self.kind):\n+ error_list.append(f\"Invalid kind. It is supposed to be {self.kind}.\")\n+ return error_list\n+\n def validate(self):\n if self.errors:\n raise ValueError(\"\\n\", self.errors)\n@@ -94,8 +126,6 @@ class Validator:\n except ValueError:\n return False\n \n- \n-\n def to_json(self):\n return {\n \"required\": self.required,\n@@ -109,25 +139,27 @@ class Validator:\n \"help_text\": self.help_text,\n \"errors\": self.errors,\n \"is_valid\": self.is_valid,\n- \"index\":self.index\n+ \"index\": self.index\n }\n \n+\n class ModelField(Validator):\n \n index = 1\n- def __init__(self,name=None,save=True, *args, **kwargs):\n- self.name = name \n+\n+ def __init__(self, name=None, save=True, *args, **kwargs):\n+ self.name = name\n self.save = save\n super().__init__(*args, **kwargs)\n \n def to_json(self):\n result = super().to_json()\n result['name'] = self.name\n- return result \n+ return result\n \n \n class CreatedField(ModelField):\n- \n+\n @property\n def initial_value(self):\n return now()\n@@ -136,67 +168,99 @@ class CreatedField(ModelField):\n if not self.value:\n self.value = now()\n \n+\n class UpdatedField(ModelField):\n \n def update(self):\n self.value = now()\n \n+\n class DeletedField(ModelField):\n \n def update(self):\n self.value = now()\n \n+\n class UUIDField(ModelField):\n- \n- @property \n+\n+ @property\n def initial_value(self):\n return str(uuid.uuid4())\n \n \n class BaseModel:\n+\n+ uid = UUIDField(name=\"uid\", required=True)\n+ created_at = CreatedField(name=\"created_at\", required=True, regex=TIMESTAMP_REGEX, place_holder=\"Created at\")\n+ updated_at = UpdatedField(name=\"updated_at\", regex=TIMESTAMP_REGEX, place_holder=\"Updated at\")\n+ deleted_at = DeletedField(name=\"deleted_at\", regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\")\n+\n+ @classmethod \n+ def from_record(cls, record, mapper):\n+ model = cls.__new__()\n+ model.mapper = mapper \n+ model.record = record\n+ return model\n+\n+ @property \n+ def mapper(self):\n+ return self._mapper \n+\n+ @mapper.setter \n+ def mapper(self, value):\n+ self._mapper = value \n+\n+ @property \n+ def record(self):\n+ return {field.name: field.value for field in self.fields}\n \n- uid = UUIDField(name=\"uid\",required=True)\n- created_at = CreatedField(name=\"created_at\",required=True, regex=TIMESTAMP_REGEX, place_holder=\"Created at\")\n- updated_at = UpdatedField(name=\"updated_at\",regex=TIMESTAMP_REGEX,place_holder=\"Updated at\")\n- deleted_at = DeletedField(name=\"deleted_at\",regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\")\n+ @record.setter \n+ def record(self, value):\n+ for key, value in self._record.items():\n+ field = self.fields.get(key)\n+ if not field:\n+ continue\n+ field.value = value\n+ return self\n \n- \n def __init__(self, *args, **kwargs):\n print(self.__dict__)\n print(dir(self.__class__))\n+ self._mapper = None\n self.fields = {}\n for key in dir(self.__class__):\n- obj = getattr(self.__class__,key)\n+ obj = getattr(self.__class__, key)\n \n- if isinstance(obj,Validator):\n+ if isinstance(obj, Validator):\n self.__dict__[key] = copy.deepcopy(obj)\n print(\"JAAA\")\n- self.__dict__[key].value = kwargs.pop(key,self.__dict__[key].initial_value)\n+ self.__dict__[key].value = kwargs.pop(key, self.__dict__[key].initial_value)\n self.fields[key] = self.__dict__[key]\n \n def __setitem__(self, key, value):\n obj = self.__dict__.get(key)\n- if isinstance(obj,Validator):\n- obj.value = value \n+ if isinstance(obj, Validator):\n+ obj.value = value\n \n def __getattr__(self, key):\n obj = self.__dict__.get(key)\n- if isinstance(obj,Validator):\n+ if isinstance(obj, Validator):\n print(\"HPAPP\")\n- return obj.value \n+ return obj.value\n return obj\n \n def set_user_data(self, data):\n for key, value in data.items():\n field = self.fields.get(key)\n if not field:\n- continue \n+ continue\n if value.get('name'):\n value = value.get('value')\n field.value = value\n- \n \n- @property \n+ \n+\n+ @property\n def is_valid(self):\n for field in self.fields.values():\n if not field.is_valid:\n@@ -205,46 +269,44 @@ class BaseModel:\n \n def __getitem__(self, key):\n obj = self.__dict__.get(key)\n- if isinstance(obj,Validator):\n- return obj.value \n+ if isinstance(obj, Validator):\n+ return obj.value\n \n def __setattr__(self, key, value):\n- obj = getattr(self,key)\n- if isinstance(obj,Validator):\n+ obj = getattr(self, key)\n+ if isinstance(obj, Validator):\n obj.value = value\n else:\n- @property \n+ self.__dict__[key] = value\n+\n+ @property\n def record(self):\n obj = self.to_json()\n record = {}\n- for key,value in obj.items():\n- if getattr(self,key).save:\n+ for key, value in obj.items():\n+ if getattr(self, key).save:\n record[key] = value.get('value')\n return record\n \n- def to_json(self,encode=False):\n+ def to_json(self, encode=False):\n model_data = OrderedDict({\n \"uid\": self.uid.value,\n \"created_at\": self.created_at.value,\n \"updated_at\": self.updated_at.value,\n \"deleted_at\": self.deleted_at.value\n })\n- \n- for key,value in self.__dict__.items(): \n+\n+ for key, value in self.__dict__.items():\n if key == \"record\":\n continue\n value = self.__dict__[key]\n- if hasattr(value,\"value\"):\n+ if hasattr(value, \"value\"):\n model_data[key] = value.to_json()\n if encode:\n- return json.dumps(model_data,indent=2)\n+ return json.dumps(model_data, indent=2)\n return model_data\n \n+\n class FormElement(ModelField):\n \n def __init__(self, place_holder=None, *args, **kwargs):\n@@ -252,15 +314,14 @@ class FormElement(ModelField):\n self.place_holder = place_holder\n \n \n-\n class FormElement(ModelField):\n \n- def __init__(self,place_holder=None, *args, **kwargs): \n- self.place_holder = place_holder \n+ def __init__(self, place_holder=None, *args, **kwargs):\n+ self.place_holder = place_holder\n super().__init__(*args, **kwargs)\n \n def to_json(self):\n data = super().to_json()\n- data[\"name\"] = self.name \n+ data[\"name\"] = self.name\n data[\"place_holder\"] = self.place_holder\n- return data \n+ return data\n\\ No newline at end of file\ndiff --git a/src/snek/system/security.py b/src/snek/system/security.py\nnew file mode 100644\nindex 0000000..b319f54\n--- /dev/null\n+++ b/src/snek/system/security.py\n@@ -0,0 +1,20 @@\n+import hashlib \n+\n+DEFAULT_SALT = b\"snekker-de-snek-\"\n+\n+async def hash(data,salt=DEFAULT_SALT):\n+ try:\n+ data = data.encode(errors=\"ignore\")\n+ except AttributeError:\n+ pass \n+ try:\n+ salt = salt.encode(errors=\"ignore\")\n+ except AttributeError:\n+ pass\n+ salted = salt + data\n+\n+ obj = hashlib.sha256(salted)\n+ return obj.hexdigest()\n+\n+async def verify(string:str, hashed:str):\n+ return await hash(string) == hashed \ndiff --git a/src/snek/system/service.py b/src/snek/system/service.py\nnew file mode 100644\nindex 0000000..5a8b553\n--- /dev/null\n+++ b/src/snek/system/service.py\n@@ -0,0 +1,40 @@\n+\n+\n+\n+from snek.mapper import get_mapper\n+from snek.system.mapper import BaseMapper \n+from snek.model.user import UserModel\n+\n+class BaseService:\n+\n+ mapper_name:BaseMapper = None\n+\n+ def __init__(self, app):\n+ self.app = app \n+ if self.mapper_name:\n+ self.mapper = get_mapper(self.mapper_name, app=self.app)\n+ else:\n+ self.mapper = None \n+\n+ async def exists(self, **kwargs):\n+ return self.mapper.exists(**kwargs)\n+ \n+ async def count(self, **kwargs):\n+ return self.mapper.count(**kwargs)\n+\n+ async def new(self, **kwargs):\n+ return await self.mapper.new()\n+\n+ async def get(self, **kwargs):\n+ return await self.mapper.get(**kwargs)\n+ \n+ async def save(self, model:UserModel):\n+ if model.is_valid:\n+ return self.mapper.save(model) and True \n+ return False \n+ \n+ async def find(self, **kwargs):\n+ return await self.mapper.find(**kwargs)\n+ \n+ async def delete(self, **kwargs):\n+ return await self.mapper.delete(**kwargs)\n\\ No newline at end of file\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nnew file mode 100644\nindex 0000000..458aa20\n--- /dev/null\n+++ b/src/snek/system/view.py\n@@ -0,0 +1,38 @@\n+from aiohttp import web\n+\n+from snek.system.markdown import render_markdown \n+\n+class BaseView(web.View):\n+ \n+ @property \n+ def app(self):\n+ return self.request.app\n+ \n+ @property\n+ def db(self):\n+ return self.app.db\n+\n+ async def json_response(self, data):\n+ return web.json_response(data)\n+\n+ async def render_template(self, template_name, context=None):\n+ if template_name.endswith(\".md\"):\n+ response = await self.request.app.render_template(template_name,self.request,context)\n+ body = await render_markdown(self.app, response.body.decode())\n+ return web.Response(body=body,content_type=\"text/html\")\n+ return await self.request.app.render_template(template_name, self.request,context)\n+ \n+class BaseFormView(BaseView):\n+\n+ form = None \n+\n+ async def get(self):\n+ form = self.form()\n+ return await self.json_response(form.to_json())\n+ \n+ async def post(self):\n+ form = self.form()\n+ post = await self.request.json()\n+ form.set_user_data(post['form'])\n+ return await self.json_response(form.to_json()) \n+\ndiff --git a/src/snek/templates/about.html b/src/snek/templates/about.html\nnew file mode 100644\nindex 0000000..0f1b8a9\n--- /dev/null\n+++ b/src/snek/templates/about.html\n@@ -0,0 +1,7 @@\n+{% extends \"base.html\" %}\n+\n+{% block main %}\n+\n+\n+\n+{% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/templates/about.md b/src/snek/templates/about.md\nnew file mode 100644\nindex 0000000..134fbc9\n--- /dev/null\n+++ b/src/snek/templates/about.md\n@@ -0,0 +1,15 @@\n+\n+A snek is a danger noodle.\n+\n+I made several design choices: \n+- Implemented **the worst 3rd party markdown to html renderer ever**. See this nice *bullet list*.\n+ - Only password requirement is thats it requires six characters. Users are responsibly for their own security. Snek is not so arrogant to determine if a password is strong enough. It's up to what user prefers. Snek does not have a forgot-my-password service tho.\n+ - Email is not required for registration. Email is (maybe) used in future for resetting password.\n+ - Homebrew made ORM framework based on dataset.\n+ - Homebrew made Form framework based on the homebrew made ORM. Most forms are ModelForms but always require an service to be saved for sake of consistency and structure.\n+ - !DRY for HMTL/jinja2 templates. For templates Snek does prefer to repeat itself to implement exceptions for a page easier. For Snek it's preffered do a few updates instead of maintaining a complex generic system that requires maintenance regarding templates.\n+ - No existing chat backend like `inspircd` (Popular decent IRC server written in the language of angels) because I prefer to know what is exactly going on above performance and concurrency limit. Also, this approach reduces as networking layer / gateway layer.\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex 5b1bdf2..9f57467 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -4,9 +4,8 @@\n \n \n {% block title %}{% endblock %}\n+ \n \n- \n- \n \n \n \ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nindex 1ee1f77..c2200fb 100644\n--- a/src/snek/templates/index.html\n+++ b/src/snek/templates/index.html\n@@ -14,7 +14,8 @@\n \n Or\n \n-\n+ Design choices\n+ See web Application so far\n \n \n \ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex c09ec70..c70d429 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -1,5 +1,5 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n- \n+ \n {% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex 61da961..f0a82e0 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -1,5 +1,5 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n- \n+ \n {% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/view/about.py b/src/snek/view/about.py\nnew file mode 100644\nindex 0000000..593d5a9\n--- /dev/null\n+++ b/src/snek/view/about.py\n@@ -0,0 +1,14 @@\n+\n+\n+from snek.system.view import BaseView\n+\n+\n+class AboutHTMLView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"about.html\")\n+ \n+class AboutMDView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"about.md\")\n\\ No newline at end of file\ndiff --git a/src/snek/view/base.py b/src/snek/view/base.py\ndeleted file mode 100644\nindex d962ee5..0000000\n--- a/src/snek/view/base.py\n+++ /dev/null\n@@ -1,31 +0,0 @@\n-from aiohttp import web \n-\n-class BaseView(web.View):\n- \n- @property \n- def app(self):\n- return self.request.app\n- \n- @property\n- def db(self):\n- return self.app.db\n-\n- def json_response(self, data):\n- return web.json_response(data)\n-\n- def render_template(self, template_name, context=None):\n- return self.request.app.render_template(template_name, self.request,context)\n- \n-class BaseFormView(BaseView):\n-\n- form = None \n-\n- async def get(self):\n- form = self.form()\n- return self.json_response(form.to_json())\n- \n- async def post(self):\n- form = self.form()\n- post = await self.request.json()\n- form.set_user_data(post['form'])\n- return self.json_response(form.to_json()) \n\\ No newline at end of file\ndiff --git a/src/snek/view/index.py b/src/snek/view/index.py\nindex a5d8b92..c7861fa 100644\n--- a/src/snek/view/index.py\n+++ b/src/snek/view/index.py\n@@ -1,4 +1,4 @@\n-from snek.view.base import BaseView\n+from snek.system.view import BaseView\n \n class IndexView(BaseView):\n \ndiff --git a/src/snek/view/login.py b/src/snek/view/login.py\nindex 3a3beaf..ffedc79 100644\n--- a/src/snek/view/login.py\n+++ b/src/snek/view/login.py\n@@ -1,5 +1,5 @@\n from snek.form.register import RegisterForm\n-from snek.view.base import BaseView \n+from snek.system.view import BaseView\n \n class LoginView(BaseView):\n \ndiff --git a/src/snek/view/login_form.py b/src/snek/view/login_form.py\nindex 26527da..e9b6eac 100644\n--- a/src/snek/view/login_form.py\n+++ b/src/snek/view/login_form.py\n@@ -1,4 +1,4 @@\n-from snek.view.base import BaseFormView\n+from snek.system.view import BaseFormView\n from snek.form.login import LoginForm\n \n class LoginFormView(BaseFormView):\ndiff --git a/src/snek/view/register.py b/src/snek/view/register.py\nindex 095b7a3..e3b3038 100644\n--- a/src/snek/view/register.py\n+++ b/src/snek/view/register.py\n@@ -1,4 +1,4 @@\n-from snek.view.base import BaseView \n+from snek.system.view import BaseView\n \n class RegisterView(BaseView):\n \ndiff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py\nindex 0ae7630..8099b01 100644\n--- a/src/snek/view/register_form.py\n+++ b/src/snek/view/register_form.py\n@@ -1,5 +1,5 @@\n from snek.form.register import RegisterForm\n-from snek.view.base import BaseFormView \n+from snek.system.view import BaseFormView\n \n class RegisterFormView(BaseFormView):\n form = RegisterForm\n\\ No newline at end of file\ndiff --git a/src/snek/view/view.py b/src/snek/view/view.py\ndeleted file mode 100644\nindex ea642a3..0000000\n--- a/src/snek/view/view.py\n+++ /dev/null\n@@ -1,6 +0,0 @@\n-from snek.view.base import BaseView \n-\n-class WebView(BaseView):\n-\n- async def get(self):\n- return await self.render_template(\"web.html\")\n\\ No newline at end of file\ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nnew file mode 100644\nindex 0000000..b06563a\n--- /dev/null\n+++ b/src/snek/view/web.py\n@@ -0,0 +1,6 @@\n+from snek.system.view import BaseView\n+\n+class WebView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"web.html\")\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Use \"/back\" URL for back button", "commit": "4b48485bcc9f73662a1eb4ab3142bb0f4d5bef7a", "diff": "commit 4b48485bcc9f73662a1eb4ab3142bb0f4d5bef7a\nAuthor: retoor \nDate: Fri Jan 24 14:05:47 2025 +0100\n\n Complete system.\n\ndiff --git a/src/snek/static/fancy-button.js b/src/snek/static/fancy-button.js\nindex 5407a3b..a3e28f8 100644\n--- a/src/snek/static/fancy-button.js\n+++ b/src/snek/static/fancy-button.js\n@@ -44,7 +44,9 @@ class FancyButton extends HTMLElement {\n const me = this \n this.buttonElement.appendChild(document.createTextNode(this.getAttribute(\"text\")))\n this.buttonElement.addEventListener(\"click\",()=>{\n- if(me.url){\n+ if(me.url == \"/back\" || me.url == \"/back/\"){\n+ window.history.back()\n+ }else if(me.url){\n window.location = me.url\n }\n })\ndiff --git a/src/snek/templates/about.html b/src/snek/templates/about.html\nindex 0f1b8a9..e403458 100644\n--- a/src/snek/templates/about.html\n+++ b/src/snek/templates/about.html\n@@ -3,5 +3,5 @@\n {% block main %}\n \n \n-\n+\n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Implement size attribute for fancy-button", "commit": "0271e3f9719a4155fc5be37c36feca865932b0c1", "diff": "commit 0271e3f9719a4155fc5be37c36feca865932b0c1\nAuthor: retoor \nDate: Fri Jan 24 14:15:55 2025 +0100\n\n Complete system.\n\ndiff --git a/src/snek/static/fancy-button.js b/src/snek/static/fancy-button.js\nindex a3e28f8..5eec215 100644\n--- a/src/snek/static/fancy-button.js\n+++ b/src/snek/static/fancy-button.js\n@@ -7,7 +7,18 @@ class FancyButton extends HTMLElement {\n constructor(){\n super()\n this.attachShadow({mode:'open'})\n+ }\n+\n+ connectedCallback() {\n+\n this.container = document.createElement('span')\n+ let size = this.getAttribute('size')\n+ console.info({GG:size})\n+ if(size == 'auto'){\n+ size = '1%' \n+ }else{\n+ size = '33%'\n+ }\n this.styleElement = document.createElement(\"style\")\n this.styleElement.innerHTML = `\n :root {\n@@ -16,7 +27,7 @@ class FancyButton extends HTMLElement {\n }\n button {\n width: var(--width);\n- min-width: 33%;\n+ min-width: ${size};\n padding: 10px;\n border: none;\n@@ -26,19 +37,20 @@ class FancyButton extends HTMLElement {\n font-weight: bold;\n cursor: pointer;\n transition: background-color 0.3s;\n+\n }\n button:hover {\n }\n `\n this.container.appendChild(this.styleElement)\n this.buttonElement = document.createElement('button')\n this.container.appendChild(this.buttonElement)\n this.shadowRoot.appendChild(this.container)\n- }\n-\n- connectedCallback() {\n+ \n this.url = this.getAttribute('url');\n this.value = this.getAttribute('value')\n const me = this \ndiff --git a/src/snek/templates/about.html b/src/snek/templates/about.html\nindex e403458..debba2a 100644\n--- a/src/snek/templates/about.html\n+++ b/src/snek/templates/about.html\n@@ -3,5 +3,5 @@\n {% block main %}\n \n \n-\n+\n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "fix: Updated login and register button URLs", "commit": "bda93e354f4691483dbbef29949672ab0989b7e0", "diff": "commit bda93e354f4691483dbbef29949672ab0989b7e0\nAuthor: retoor \nDate: Fri Jan 24 14:16:52 2025 +0100\n\n Complete system.\n\ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nindex c2200fb..8157f3d 100644\n--- a/src/snek/templates/index.html\n+++ b/src/snek/templates/index.html\n@@ -11,9 +11,9 @@\n \n
\n

Snek

\n- \n+ \n Or\n- \n+ \n Design choices\n See web Application so far\n
"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Style adjustments and layout improvements for responsive design", "commit": "757b67b78c2f396df7ac7b5706f98833aedfb85b", "diff": "commit 757b67b78c2f396df7ac7b5706f98833aedfb85b\nAuthor: retoor \nDate: Fri Jan 24 14:47:19 2025 +0100\n\n CSS.\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex 363e4f1..263f1eb 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -4,6 +4,7 @@\n box-sizing: border-box;\n }\n \n+\n body {\n font-family: Arial, sans-serif;\n@@ -12,6 +13,10 @@ body {\n display: flex;\n flex-direction: column;\n height: 100vh;\n+ min-width: 100%;\n+}\n+main {\n+ min-width: 100%;\n }\n \n header {\ndiff --git a/src/snek/static/style.css b/src/snek/static/style.css\nindex 990fcf9..008a3ee 100644\n--- a/src/snek/static/style.css\n+++ b/src/snek/static/style.css\n@@ -1,3 +1,27 @@\n+\n+* {\n+ margin: 0;\n+ padding: 0;\n+ box-sizing: border-box;\n+ }\n+\n+.center {\n+ padding: 30px;\n+ \n+ text-align: center;\n+ margin: auto auto;\n+ left: 25%;\n+ position: absolute;\n+ }\n+\n+ @media screen and (max-width: 500px) {\n+ .center {\n+ width: 100%;\n+ left: 0px;\n+ }\n+ \n+ }\n+\n h1 {\n font-size: 2em;\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex 9f57467..8637867 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -18,11 +18,6 @@\n \n
\n
\n- \n {% block main %}\n {% endblock %}\n
\ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex c70d429..2fe1a73 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -1,5 +1,7 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n- \n+\n+ \n+\n {% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex f0a82e0..3668f13 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -1,5 +1,5 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n- \n+ \n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor styling and layout for improved aesthetics and responsiveness", "commit": "6ba6121988dae019d4c4c0a3b8592b443f094065", "diff": "commit 6ba6121988dae019d4c4c0a3b8592b443f094065\nAuthor: retoor \nDate: Fri Jan 24 15:20:35 2025 +0100\n\n CSS.\n\ndiff --git a/src/snek/form/login.py b/src/snek/form/login.py\nindex a87f7d3..910cd73 100644\n--- a/src/snek/form/login.py\n+++ b/src/snek/form/login.py\n@@ -21,4 +21,5 @@ class LoginForm(Form):\n text=\"Login\",\n type=\"button\"\n )\n+ \n \ndiff --git a/src/snek/static/html-frame.js b/src/snek/static/html-frame.js\nindex 0d5d4c9..75f800e 100644\n--- a/src/snek/static/html-frame.js\n+++ b/src/snek/static/html-frame.js\n@@ -30,9 +30,7 @@ class HTMLFrame extends HTMLElement {\n const parent = this\n const markdownElement = document.createElement('div')\n markdownElement.innerHTML = html\n- document.body.appendChild(markdownElement)\n- \n+ this.outerHTML = html\n }else{\n this.container.innerHTML = html;\n }\ndiff --git a/src/snek/static/style.css b/src/snek/static/style.css\nindex 008a3ee..d2cda8c 100644\n--- a/src/snek/static/style.css\n+++ b/src/snek/static/style.css\n@@ -1,18 +1,18 @@\n \n * {\n- margin: 0;\n- padding: 0;\n+ \n box-sizing: border-box;\n }\n \n-.center {\n+ .dialog {\n+\n+ border-radius: 10px;\n padding: 30px;\n- \n- text-align: center;\n- margin: auto auto;\n- left: 25%;\n- position: absolute;\n- }\n+ width: 800px;\n+ margin: 30px;\n+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n+}\n \n @media screen and (max-width: 500px) {\n .center {\n@@ -34,8 +34,15 @@ h2 {\n margin-bottom: 20px;\n }\n body {\n+ font-family: Arial, sans-serif;\n+ line-height: 1.5;\n+ display: flex;\n+ flex-direction: column;\n+ justify-content: center;\n+ align-items: center;\n+ min-height: 100vh;\n \n }\n div {\ndiff --git a/src/snek/templates/about.html b/src/snek/templates/about.html\nindex debba2a..b68396e 100644\n--- a/src/snek/templates/about.html\n+++ b/src/snek/templates/about.html\n@@ -1,7 +1,10 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n+
\n \n+ \n \n-\n+\n+
\n {% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex 2fe1a73..3e72928 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -1,7 +1,7 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n-\n+ \n \n \n {% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex 3668f13..b1540ea 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -1,5 +1,7 @@\n {% extends \"base.html\" %}\n \n {% block main %}\n+\n+ \n \n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor login and register form endpoints to use /login.json and /register.json", "commit": "c1eeacc0b415ef770418bb053d18ac0ffa4f64c2", "diff": "commit c1eeacc0b415ef770418bb053d18ac0ffa4f64c2\nAuthor: retoor \nDate: Fri Jan 24 16:08:56 2025 +0100\n\n Caching.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex deac5d3..db9ebf9 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -2,7 +2,7 @@ import pathlib\n \n from aiohttp import web\n from app.app import Application as BaseApplication\n-\n+from app.cache import time_cache_async\n from jinja_markdown2 import MarkdownExtension\n from snek.system import http\n from snek.system.middleware import cors_middleware\n@@ -41,10 +41,9 @@ class Application(BaseApplication):\n self.router.add_view(\"/about.md\", AboutMDView)\n self.router.add_view(\"/web.html\", WebView)\n self.router.add_view(\"/login.html\", LoginView)\n- self.router.add_view(\"/login-form.json\", LoginFormView)\n+ self.router.add_view(\"/login.json\", LoginFormView)\n self.router.add_view(\"/register.html\", RegisterView)\n- \n- self.router.add_view(\"/register-form.json\", RegisterFormView)\n+ self.router.add_view(\"/register.json\", RegisterFormView)\n self.router.add_get(\"/http-get\", self.handle_http_get)\n self.router.add_get(\"/http-photo\", self.handle_http_photo)\n \n@@ -65,6 +64,11 @@ class Application(BaseApplication):\n return web.Response(\n body=path.read_bytes(), headers={\"Content-Type\": \"image/png\"}\n )\n+ \n+ \n+ @time_cache_async(60)\n+ async def render_template(self, template, request, context=None):\n+ return await super().render_template(template, request, context)\n \n \n app = Application()\ndiff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py\nindex bded949..93bc08c 100644\n--- a/src/snek/system/markdown.py\n+++ b/src/snek/system/markdown.py\n@@ -8,7 +8,7 @@ from pygments import highlight\n from pygments.lexers import get_lexer_by_name\n from pygments.formatters import html\n from pygments.styles import get_style_by_name\n-\n+import functools\n \n class MarkdownRenderer(HTMLRenderer):\n def __init__(self, app, template):\n@@ -37,7 +37,11 @@ class MarkdownRenderer(HTMLRenderer):\n return markdown(markdown_string)\n \n \n+@functools.cache\n+def render_markdown_sync(app, markdown_string):\n+ renderer = MarkdownRenderer(app,None)\n+ markdown = Markdown(renderer=renderer)\n+ return markdown(markdown_string)\n+\n async def render_markdown(app, markdown_string):\n- renderer = MarkdownRenderer(app,None)\n- markdown = Markdown(renderer=renderer)\n- return markdown(markdown_string)\n\\ No newline at end of file\n+ return render_markdown_sync(app,markdown_string)\n\\ No newline at end of file\ndiff --git a/src/snek/system/model.py b/src/snek/system/model.py\nindex eb490b3..9a00186 100644\n--- a/src/snek/system/model.py\n+++ b/src/snek/system/model.py\n@@ -81,8 +81,6 @@ class Validator:\n self.regex = regex\n self._value = None\n self.value = value\n- print(\"xxxx\", value, flush=True)\n-\n self.kind = kind\n self.help_text = help_text\n self.__dict__.update(kwargs)\n@@ -106,7 +104,6 @@ class Validator:\n error_list.append(f\"Field should be minimal {self.min_length} characters long.\")\n if self.max_length is not None and len(self.value) > self.max_length:\n error_list.append(f\"Field should be maximal {self.max_length} characters long.\")\n- print(self.regex, self.value, flush=True)\n if self.regex and self.value and not re.match(self.regex, self.value):\n error_list.append(\"Invalid value.\")\n if self.kind and not isinstance(self.value, self.kind):\n@@ -224,8 +221,6 @@ class BaseModel:\n return self\n \n def __init__(self, *args, **kwargs):\n- print(self.__dict__)\n- print(dir(self.__class__))\n self._mapper = None\n self.fields = {}\n for key in dir(self.__class__):\n@@ -233,7 +228,6 @@ class BaseModel:\n \n if isinstance(obj, Validator):\n self.__dict__[key] = copy.deepcopy(obj)\n- print(\"JAAA\")\n self.__dict__[key].value = kwargs.pop(key, self.__dict__[key].initial_value)\n self.fields[key] = self.__dict__[key]\n \n@@ -245,7 +239,6 @@ class BaseModel:\n def __getattr__(self, key):\n obj = self.__dict__.get(key)\n if isinstance(obj, Validator):\n- print(\"HPAPP\")\n return obj.value\n return obj\n \ndiff --git a/src/snek/templates/about.md b/src/snek/templates/about.md\nindex 134fbc9..c28e9d0 100644\n--- a/src/snek/templates/about.md\n+++ b/src/snek/templates/about.md\n@@ -13,3 +13,57 @@ I made several design choices:\n - Homebrew made Form framework based on the homebrew made ORM. Most forms are ModelForms but always require an service to be saved for sake of consistency and structure.\n - !DRY for HMTL/jinja2 templates. For templates Snek does prefer to repeat itself to implement exceptions for a page easier. For Snek it's preffered do a few updates instead of maintaining a complex generic system that requires maintenance regarding templates.\n - No existing chat backend like `inspircd` (Popular decent IRC server written in the language of angels) because I prefer to know what is exactly going on above performance and concurrency limit. Also, this approach reduces as networking layer / gateway layer.\n+\n+\n+A few examples of how the system framework works.\n+\n+```python\n+\n+new_user_object = await app.service.user.register(\n+ username=\"retoor\", \n+ password=\"retoorded\"\n+)\n+```\n+\n+```python\n+from snek.system import security\n+\n+var1 = security.encrypt(\"data\")\n+var2 = security.encrypt(b\"data\")\n+\n+assert(var1 == var2)\n+```\n+\n+```python\n+from snek.system.view import BaseView \n+\n+class IndexView(BaseView):\n+ \n+ async def get(self):\n+ return await self.render(\"index.html\")\n+```\n+```python\n+from snek.system.view import BaseFormView\n+from snek.form.register import RegisterForm\n+\n+class RegisterFormView(BaseFormView):\n+ \n+ form = RegisterForm\n+```\n+```python\n+app.routes.add_view(\"/your-page.html\", YourViewClass)\n+```\n\\ No newline at end of file\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex 8637867..e00c886 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -4,6 +4,7 @@\n \n \n {% block title %}{% endblock %}\n+ \n \n \n \ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex 3e72928..0a6bcc1 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -2,6 +2,6 @@\n \n {% block main %}\n \n- \n+ \n \n {% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex b1540ea..f8d1067 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -3,5 +3,5 @@\n {% block main %}\n \n \n- \n+ \n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Implemented caching decorator for asynchronous functions", "commit": "21ab5628b072320ae0851819116e57539b2397d9", "diff": "commit 21ab5628b072320ae0851819116e57539b2397d9\nAuthor: retoor \nDate: Fri Jan 24 16:09:10 2025 +0100\n\n Caching.\n\ndiff --git a/src/snek/system/cache.py b/src/snek/system/cache.py\nnew file mode 100644\nindex 0000000..2992803\n--- /dev/null\n+++ b/src/snek/system/cache.py\n@@ -0,0 +1,17 @@\n+\n+import functools \n+\n+cache = functools.cache\n+\n+def async_cache(func):\n+ cache = {}\n+\n+ @functools.wraps(func)\n+ async def wrapper(*args):\n+ if args in cache:\n+ return cache[args]\n+ result = await func(*args)\n+ cache[args] = result\n+ return result\n+\n+ return wrapper\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Switch to gunicorn and add docs and about pages", "commit": "8486c22c325ba358bd48766c518f7c7bd30059eb", "diff": "commit 8486c22c325ba358bd48766c518f7c7bd30059eb\nAuthor: retoor \nDate: Fri Jan 24 16:33:27 2025 +0100\n\n Disabled cache.\n\ndiff --git a/compose.yml b/compose.yml\nindex 3b1f650..24e186c 100644\n--- a/compose.yml\n+++ b/compose.yml\n@@ -6,5 +6,6 @@ services:\n - \"8081:8081\"\n volumes:\n - ./:/code\n- entrypoint: [\"python\",\"-m\",\"snek.app\"]\n+ entrypoint: [\"gunicorn\", \"-w\", \"1\", \"-k\", \"aiohttp.worker.GunicornWebWorker\", \"snek.gunicorn:app\",\"--bind\",\"0.0.0.0:8081\"]\n \n\\ No newline at end of file\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex db9ebf9..f25ffba 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -7,6 +7,7 @@ from jinja_markdown2 import MarkdownExtension\n from snek.system import http\n from snek.system.middleware import cors_middleware\n from snek.view.about import AboutHTMLView, AboutMDView\n+from snek.view.docs import DocsHTMLView, DocsMDView\n from snek.view.index import IndexView\n from snek.view.login import LoginView\n from snek.view.login_form import LoginFormView\n@@ -39,6 +40,9 @@ class Application(BaseApplication):\n )\n self.router.add_view(\"/about.html\", AboutHTMLView)\n self.router.add_view(\"/about.md\", AboutMDView)\n+ self.router.add_view(\"/docs.html\", DocsHTMLView)\n+ self.router.add_view(\"/docs.md\", DocsMDView)\n+ \n self.router.add_view(\"/web.html\", WebView)\n self.router.add_view(\"/login.html\", LoginView)\n self.router.add_view(\"/login.json\", LoginFormView)\n@@ -66,7 +70,7 @@ class Application(BaseApplication):\n )\n \n \n- @time_cache_async(60)\n async def render_template(self, template, request, context=None):\n return await super().render_template(template, request, context)\n \ndiff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py\nindex 93bc08c..489a3e5 100644\n--- a/src/snek/system/markdown.py\n+++ b/src/snek/system/markdown.py\n@@ -9,6 +9,7 @@ from pygments.lexers import get_lexer_by_name\n from pygments.formatters import html\n from pygments.styles import get_style_by_name\n import functools\n+from app.cache import time_cache_async\n \n class MarkdownRenderer(HTMLRenderer):\n def __init__(self, app, template):\n@@ -28,7 +29,6 @@ class MarkdownRenderer(HTMLRenderer):\n lexer = get_lexer_by_name(lang, stripall=True)\n formatter = html.HtmlFormatter(lineseparator=\"
\")\n- print(code, lang,info, flush=True)\n return highlight(code, lexer, formatter)\n def render(self):\n markdown_string = self.app.template_path.joinpath(self.template).read_text()\n@@ -37,11 +37,12 @@ class MarkdownRenderer(HTMLRenderer):\n return markdown(markdown_string)\n \n \n-@functools.cache\n+\n def render_markdown_sync(app, markdown_string):\n renderer = MarkdownRenderer(app,None)\n markdown = Markdown(renderer=renderer)\n return markdown(markdown_string)\n \n+@time_cache_async(120)\n async def render_markdown(app, markdown_string):\n return render_markdown_sync(app,markdown_string)\n\\ No newline at end of file\ndiff --git a/src/snek/templates/about.md b/src/snek/templates/about.md\nindex c28e9d0..0b43fb4 100644\n--- a/src/snek/templates/about.md\n+++ b/src/snek/templates/about.md\n@@ -14,56 +14,3 @@ I made several design choices:\n - !DRY for HMTL/jinja2 templates. For templates Snek does prefer to repeat itself to implement exceptions for a page easier. For Snek it's preffered do a few updates instead of maintaining a complex generic system that requires maintenance regarding templates.\n - No existing chat backend like `inspircd` (Popular decent IRC server written in the language of angels) because I prefer to know what is exactly going on above performance and concurrency limit. Also, this approach reduces as networking layer / gateway layer.\n \n-\n-A few examples of how the system framework works.\n-\n-```python\n-\n-new_user_object = await app.service.user.register(\n- username=\"retoor\", \n- password=\"retoorded\"\n-)\n-```\n-\n-```python\n-from snek.system import security\n-\n-var1 = security.encrypt(\"data\")\n-var2 = security.encrypt(b\"data\")\n-\n-assert(var1 == var2)\n-```\n-\n-```python\n-from snek.system.view import BaseView \n-\n-class IndexView(BaseView):\n- \n- async def get(self):\n- return await self.render(\"index.html\")\n-```\n-```python\n-from snek.system.view import BaseFormView\n-from snek.form.register import RegisterForm\n-\n-class RegisterFormView(BaseFormView):\n- \n- form = RegisterForm\n-```\n-```python\n-app.routes.add_view(\"/your-page.html\", YourViewClass)\n-```\n\\ No newline at end of file\ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nindex 8157f3d..ad02a6a 100644\n--- a/src/snek/templates/index.html\n+++ b/src/snek/templates/index.html\n@@ -15,7 +15,8 @@\n Or\n \n Design choices\n- See web Application so far\n+ App preview\n+ API docs\n \n \n "} +{"repo": ".", "date": "2025-01-24", "line": "feat: Added API documentation and corresponding views", "commit": "aecd9f844ef0a277a55aa536db3336362e8db353", "diff": "commit aecd9f844ef0a277a55aa536db3336362e8db353\nAuthor: retoor \nDate: Fri Jan 24 16:34:02 2025 +0100\n\n Docs.\n\ndiff --git a/src/snek/templates/docs.html b/src/snek/templates/docs.html\nnew file mode 100644\nindex 0000000..21c0309\n--- /dev/null\n+++ b/src/snek/templates/docs.html\n@@ -0,0 +1,10 @@\n+{% extends \"base.html\" %}\n+\n+{% block main %}\n+
\n+\n+ \n+\n+\n+
\n+{% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/templates/docs.md b/src/snek/templates/docs.md\nnew file mode 100644\nindex 0000000..2cf60cb\n--- /dev/null\n+++ b/src/snek/templates/docs.md\n@@ -0,0 +1,53 @@\n+\n+Currently only some details about the internal API are available.\n+\n+```python\n+\n+new_user_object = await app.service.user.register(\n+ username=\"retoor\", \n+ password=\"retoorded\"\n+)\n+```\n+\n+```python\n+from snek.system import security\n+\n+var1 = security.encrypt(\"data\")\n+var2 = security.encrypt(b\"data\")\n+\n+assert(var1 == var2)\n+```\n+\n+```python\n+from snek.system.view import BaseView \n+\n+class IndexView(BaseView):\n+ \n+ async def get(self):\n+ return await self.render(\"index.html\")\n+```\n+```python\n+from snek.system.view import BaseFormView\n+from snek.form.register import RegisterForm\n+\n+class RegisterFormView(BaseFormView):\n+ \n+ form = RegisterForm\n+```\n+```python\n+app.routes.add_view(\"/your-page.html\", YourViewClass)\n+```\n\\ No newline at end of file\ndiff --git a/src/snek/view/docs.py b/src/snek/view/docs.py\nnew file mode 100644\nindex 0000000..e69d754\n--- /dev/null\n+++ b/src/snek/view/docs.py\n@@ -0,0 +1,15 @@\n+\n+\n+\n+from snek.system.view import BaseView\n+\n+\n+class DocsHTMLView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"docs.html\")\n+ \n+class DocsMDView(BaseView):\n+\n+ async def get(self):\n+ return await self.render_template(\"docs.md\")\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "docs: Added styling for dialog elements", "commit": "be9489f939b3518f9c3a73b9e54ba0f9d34ae24c", "diff": "commit be9489f939b3518f9c3a73b9e54ba0f9d34ae24c\nAuthor: retoor \nDate: Fri Jan 24 16:43:57 2025 +0100\n\n Docs.\n\ndiff --git a/src/snek/static/style.css b/src/snek/static/style.css\nindex d2cda8c..63a28ed 100644\n--- a/src/snek/static/style.css\n+++ b/src/snek/static/style.css\n@@ -19,6 +19,10 @@\n width: 100%;\n left: 0px;\n }\n+ .dialog {\n+ width: 100%;\n+ left: 0px;\n+ }\n \n }"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Implemented user registration with username availability check and email field", "commit": "2ba55f692dfc1b60ac55d514b182fb8834cb99bb", "diff": "commit 2ba55f692dfc1b60ac55d514b182fb8834cb99bb\nAuthor: retoor \nDate: Fri Jan 24 21:19:03 2025 +0100\n\n Finished register.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex f25ffba..0cca574 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -4,6 +4,8 @@ from aiohttp import web\n from app.app import Application as BaseApplication\n from app.cache import time_cache_async\n from jinja_markdown2 import MarkdownExtension\n+from snek.mapper import get_mappers\n+from snek.service import get_services\n from snek.system import http\n from snek.system.middleware import cors_middleware\n from snek.view.about import AboutHTMLView, AboutMDView\n@@ -14,6 +16,7 @@ from snek.view.login_form import LoginFormView\n from snek.view.register import RegisterView\n from snek.view.register_form import RegisterFormView\n from snek.view.web import WebView\n+from types import SimpleNamespace\n \n \n class Application(BaseApplication):\n@@ -29,6 +32,11 @@ class Application(BaseApplication):\n )\n self.jinja2_env.add_extension(MarkdownExtension)\n self.setup_router()\n+ self.setup_services()\n+\n+ def setup_services(self):\n+ self.services = SimpleNamespace(**get_services(app=self))\n+ self.mappers = SimpleNamespace(**get_mappers(app=self))\n \n def setup_router(self):\n self.router.add_get(\"/\", IndexView)\ndiff --git a/src/snek/form/login.py b/src/snek/form/login.py\nindex 910cd73..0d97d41 100644\n--- a/src/snek/form/login.py\n+++ b/src/snek/form/login.py\n@@ -22,4 +22,3 @@ class LoginForm(Form):\n type=\"button\"\n )\n \n-\ndiff --git a/src/snek/form/register.py b/src/snek/form/register.py\nindex 7dff3e4..4252bf1 100644\n--- a/src/snek/form/register.py\n+++ b/src/snek/form/register.py\n@@ -1,10 +1,19 @@\n from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement\n \n+class UsernameField(FormInputElement):\n+\n+ @property\n+ async def errors(self):\n+ result = await super().errors\n+ if self.value and await self.app.services.user.count(username=self.value):\n+ result.append(\"Username is not available.\")\n+ return result\n+\n class RegisterForm(Form):\n \n title = HTMLElement(tag=\"h1\", text=\"Register\")\n \n- username = FormInputElement(\n+ username = UsernameField(\n name=\"username\", \n required=True,\n min_length=2,\ndiff --git a/src/snek/mapper/user.py b/src/snek/mapper/user.py\nindex 5b8671e..642028c 100644\n--- a/src/snek/mapper/user.py\n+++ b/src/snek/mapper/user.py\n@@ -3,4 +3,4 @@ from snek.model.user import UserModel\n \n class UserMapper(BaseMapper):\n table_name = \"user\"\n- model: UserModel \n\\ No newline at end of file\n+ model_class = UserModel \n\\ No newline at end of file\ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nindex cde4b8c..a2b0cb1 100644\n--- a/src/snek/service/user.py\n+++ b/src/snek/service/user.py\n@@ -1,13 +1,14 @@\n from snek.system.service import BaseService \n from snek.system import security \n \n-class UserService:\n+class UserService(BaseService):\n mapper_name = \"user\"\n \n- async def create_user(self, username, password):\n+ async def register(self, email, username, password):\n if await self.exists(username=username):\n raise Exception(\"User already exists.\")\n model = await self.new()\n+ model.email = email\n model.username = username\n model.password = await security.hash(password)\n if await self.save(model):\ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nindex 58a67e2..a47c6db 100644\n--- a/src/snek/static/generic-form.js\n+++ b/src/snek/static/generic-form.js\n@@ -280,7 +280,11 @@ class GenericForm extends HTMLElement {\n if(e.detail.type == \"button\"){\n if(e.detail.value == \"submit\")\n {\n- await me.validate()\n+ const isValid = await me.validate()\n+ if(isValid){\n+ const isProcessed = await me.submit()\n+ console.info({processed:isProcessed})\n+ }\n }\n }\n \n@@ -294,13 +298,15 @@ class GenericForm extends HTMLElement {\n async validate(){\n const url = this.getAttribute(\"url\")\n const me = this\n- const response = await fetch(url,{\n+ let response = await fetch(url,{\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\"action\":\"validate\", \"form\":me.form})\n });\n+\n+ \n const form = await response.json()\n Object.values(form.fields).forEach(field=>{\n if(!me.form.fields[field.name])\n@@ -320,6 +326,22 @@ class GenericForm extends HTMLElement {\n console.info(field.errors)\n me.fields[field.name].setErrors(field.errors)\n })\n+ console.info({XX:form})\n+ return form['is_valid']\n+ }\n+ async submit(){\n+ const me = this \n+ const url = me.getAttribute(\"url\")\n+ const response = await fetch(url,{\n+ method: 'POST',\n+ headers: {\n+ 'Content-Type': 'application/json'\n+ },\n+ body: JSON.stringify({\"action\":\"submit\", \"form\":me.form})\n+ });\n+ return await response.json()\n+ \n }\n+ \n }\n customElements.define('generic-form', GenericForm);\n\\ No newline at end of file\ndiff --git a/src/snek/system/form.py b/src/snek/system/form.py\nindex f9ebebb..82091b6 100644\n--- a/src/snek/system/form.py\n+++ b/src/snek/system/form.py\n@@ -35,8 +35,8 @@ class HTMLElement(model.ModelField):\n self.html = html\n super().__init__(name=name, *args, **kwargs)\n \n- def to_json(self):\n- result = super().to_json()\n+ async def to_json(self):\n+ result = await super().to_json()\n result['text'] = self.text\n result['id'] = self.id\n result['html'] = self.html\n@@ -53,8 +53,8 @@ class FormInputElement(FormElement):\n self.place_holder = place_holder\n self.type = type\n \n- def to_json(self):\n- data = super().to_json()\n+ async def to_json(self):\n+ data = await super().to_json()\n data[\"place_holder\"] = self.place_holder\n data[\"type\"] = self.type\n return data\n@@ -66,31 +66,36 @@ class FormButtonElement(FormElement):\n class Form(model.BaseModel):\n @property\n def html_elements(self):\n- json_elements = super().to_json()\n return [element for element in self.fields if isinstance(element, HTMLElement)]\n \n def set_user_data(self, data):\n return super().set_user_data(data.get('fields'))\n \n- def to_json(self, encode=False):\n- elements = super().to_json()\n+ async def to_json(self, encode=False):\n+ elements = await super().to_json()\n html_elements = {}\n for element in elements.keys():\n+ if element == 'is_valid':\n+ continue \n field = getattr(self, element)\n if isinstance(field, HTMLElement):\n try:\n html_elements[element] = elements[element]\n except KeyError:\n pass\n- return dict(fields=html_elements, is_valid=self.is_valid, errors=self.errors)\n+\n+ is_valid = all(field['is_valid'] for field in html_elements.values())\n+ return dict(fields=html_elements, is_valid=is_valid, errors=await self.errors)\n \n @property\n- def errors(self):\n+ async def errors(self):\n result = []\n for field in self.html_elements:\n- result += field.errors\n+ result += await field.errors\n return result\n \n @property\n- def is_valid(self):\n- return all(element.is_valid for element in self.html_elements)\n\\ No newline at end of file\n+ async def is_valid(self):\n+ return False\n\\ No newline at end of file\ndiff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py\nindex f4beb2e..667aad1 100644\n--- a/src/snek/system/mapper.py\n+++ b/src/snek/system/mapper.py\n@@ -1,25 +1,19 @@\n \n DEFAULT_LIMIT = 30\n+import typing\n from snek.system.model import BaseModel\n-from snek.app import Application \n+\n import types\n \n-class Mapper:\n+class BaseMapper:\n \n model_class:BaseModel = None \n default_limit:int = DEFAULT_LIMIT\n table_name:str = None \n \n- def __init__(self, app:Application, table_name:str, model_class:BaseModel):\n+ def __init__(self, app):\n self.app = app \n \n- if not self.model_class:\n- raise ValueError(\"Mapper configuration error: model_class is not set.\")\n- self.model_class = model_class \n- \n- self.table_name = table_name\n- if not self.table_name:\n- raise ValueError(\"Mapper configuration error: table_name is not set.\")\n self.default_limit = self.__class__.default_limit \n \n @property\n@@ -33,12 +27,12 @@ class Mapper:\n def table(self):\n return self.db[self.table_name]\n \n- async def get(self, uid:str=None, **kwargs) -> types.Optional[BaseModel]\n+ async def get(self, uid:str=None, **kwargs) -> BaseModel:\n if uid:\n kwargs['uid'] = uid \n model = self.new()\n record = self.table.find_one(**kwargs)\n- return self.model_class.from_record(mapper=self,record=record)\n+ return await self.model_class.from_record(mapper=self,record=record)\n \n async def exists(self, **kwargs):\n return self.table.exists(**kwargs)\n@@ -47,16 +41,16 @@ class Mapper:\n return self.table.count(**kwargs)\n \n async def save(self, model:BaseModel) -> bool:\n- record = model.record\n+ record = await model.record\n if not record.get('uid'):\n raise Exception(f\"Attempt to save without uid: {record}.\")\n return self.table.upsert(record,['uid'])\n \n- async def find(self, **kwargs) -> types.List[BaseModel]:\n+ async def find(self, **kwargs) -> typing.AsyncGenerator:\n if not kwargs.get(\"_limit\"):\n kwargs[\"_limit\"] = self.default_limit\n for record in self.table.find(**kwargs):\n- yield self.model_class.from_record(mapper=self,record=record)\n+ yield await self.model_class.from_record(mapper=self,record=record)\n \n async def delete(self, kwargs=None)-> int: \n if not kwargs or not isinstance(kwargs, dict):\ndiff --git a/src/snek/system/model.py b/src/snek/system/model.py\nindex 9a00186..0d700ff 100644\n--- a/src/snek/system/model.py\n+++ b/src/snek/system/model.py\n@@ -70,9 +70,11 @@ class Validator:\n def custom_validation(self):\n return True\n \n- def __init__(self, required=False, min_num=None, max_num=None, min_length=None, max_length=None, regex=None, value=None, kind=None, help_text=None, **kwargs):\n+ def __init__(self, required=False, min_num=None, max_num=None, min_length=None, max_length=None, regex=None, value=None, kind=None, help_text=None, app=None, model=None, **kwargs):\n self.index = Validator._index\n Validator._index += 1\n+ self.app = app\n+ self.model = model\n self.required = required\n self.min_num = min_num\n self.max_num = max_num\n@@ -86,7 +88,7 @@ class Validator:\n self.__dict__.update(kwargs)\n \n @property\n- def errors(self):\n+ async def errors(self):\n error_list = []\n if self.value is None and self.required:\n error_list.append(\"Field is required.\")\n@@ -110,20 +112,23 @@ class Validator:\n error_list.append(f\"Invalid kind. It is supposed to be {self.kind}.\")\n return error_list\n \n- def validate(self):\n- if self.errors:\n- raise ValueError(\"\\n\", self.errors)\n+ async def validate(self):\n+ errors = await self.errors\n+ if errors:\n+ raise ValueError(f\"Errors: {errors}.\")\n return True\n \n @property\n- def is_valid(self):\n+ async def is_valid(self):\n try:\n- self.validate()\n+ await self.validate()\n return True\n except ValueError:\n return False\n \n- def to_json(self):\n+ async def to_json(self):\n+ errors = await self.errors\n+ is_valid = await self.is_valid\n return {\n \"required\": self.required,\n \"min_num\": self.min_num,\n@@ -134,8 +139,8 @@ class Validator:\n \"value\": self.value,\n \"kind\": str(self.kind),\n \"help_text\": self.help_text,\n- \"errors\": self.errors,\n- \"is_valid\": self.is_valid,\n+ \"errors\": errors,\n+ \"is_valid\": is_valid,\n \"index\": self.index\n }\n \n@@ -149,8 +154,8 @@ class ModelField(Validator):\n self.save = save\n super().__init__(*args, **kwargs)\n \n- def to_json(self):\n- result = super().to_json()\n+ async def to_json(self):\n+ result = await super().to_json()\n result['name'] = self.name\n return result\n \n@@ -193,7 +198,7 @@ class BaseModel:\n deleted_at = DeletedField(name=\"deleted_at\", regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\")\n \n @classmethod \n- def from_record(cls, record, mapper):\n+ async def from_record(cls, record, mapper):\n model = cls.__new__()\n model.mapper = mapper \n model.record = record\n@@ -230,6 +235,8 @@ class BaseModel:\n self.__dict__[key] = copy.deepcopy(obj)\n self.__dict__[key].value = kwargs.pop(key, self.__dict__[key].initial_value)\n self.fields[key] = self.__dict__[key]\n+ self.fields[key].model = self\n+ self.fields[key].app = kwargs.get('app')\n \n def __setitem__(self, key, value):\n obj = self.__dict__.get(key)\n@@ -254,11 +261,9 @@ class BaseModel:\n \n \n @property\n- def is_valid(self):\n- for field in self.fields.values():\n- if not field.is_valid:\n- return False\n- return True\n+ async def is_valid(self):\n+ return all([await field.is_valid for field in self.fields.values()])\n+ \n \n def __getitem__(self, key):\n obj = self.__dict__.get(key)\n@@ -273,28 +278,31 @@ class BaseModel:\n self.__dict__[key] = value\n \n @property\n- def record(self):\n- obj = self.to_json()\n+ async def record(self):\n+ obj = await self.to_json()\n record = {}\n for key, value in obj.items():\n+ if not isinstance(value, dict) or not 'value' in value:\n+ continue\n if getattr(self, key).save:\n record[key] = value.get('value')\n return record\n \n- def to_json(self, encode=False):\n+ async def to_json(self, encode=False):\n model_data = OrderedDict({\n \"uid\": self.uid.value,\n \"created_at\": self.created_at.value,\n \"updated_at\": self.updated_at.value,\n- \"deleted_at\": self.deleted_at.value\n+ \"deleted_at\": self.deleted_at.value,\n+ \"is_valid\": await self.is_valid\n })\n \n- for key, value in self.__dict__.items():\n+ for key, value in self.fields.items():\n if key == \"record\":\n continue\n value = self.__dict__[key]\n if hasattr(value, \"value\"):\n- model_data[key] = value.to_json()\n+ model_data[key] = await value.to_json()\n if encode:\n return json.dumps(model_data, indent=2)\n return model_data\n@@ -313,8 +321,8 @@ class FormElement(ModelField):\n self.place_holder = place_holder\n super().__init__(*args, **kwargs)\n \n- def to_json(self):\n- data = super().to_json()\n+ async def to_json(self):\n+ data = await super().to_json()\n data[\"name\"] = self.name\n data[\"place_holder\"] = self.place_holder\n return data\n\\ No newline at end of file\ndiff --git a/src/snek/system/service.py b/src/snek/system/service.py\nindex 5a8b553..d970b5f 100644\n--- a/src/snek/system/service.py\n+++ b/src/snek/system/service.py\n@@ -17,10 +17,10 @@ class BaseService:\n self.mapper = None \n \n async def exists(self, **kwargs):\n- return self.mapper.exists(**kwargs)\n+ return await self.count(**kwargs) > 0\n \n async def count(self, **kwargs):\n- return self.mapper.count(**kwargs)\n+ return await self.mapper.count(**kwargs)\n \n async def new(self, **kwargs):\n return await self.mapper.new()\n@@ -29,9 +29,9 @@ class BaseService:\n return await self.mapper.get(**kwargs)\n \n async def save(self, model:UserModel):\n- if model.is_valid:\n- return self.mapper.save(model) and True \n- return False \n+ return await self.mapper.save(model) and True \n+ \n \n async def find(self, **kwargs):\n return await self.mapper.find(**kwargs)\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nindex 458aa20..3b53f33 100644\n--- a/src/snek/system/view.py\n+++ b/src/snek/system/view.py\n@@ -27,12 +27,21 @@ class BaseFormView(BaseView):\n form = None \n \n async def get(self):\n- form = self.form()\n- return await self.json_response(form.to_json())\n+ form = self.form(app=self.app)\n+ \n+ return await self.json_response(await form.to_json())\n \n async def post(self):\n- form = self.form()\n+ form = self.form(app=self.app)\n post = await self.request.json()\n form.set_user_data(post['form'])\n- return await self.json_response(form.to_json()) \n+ result = await form.to_json()\n+ if post.get('action') == 'validate':\n+ pass\n+ if post.get('action') == 'submit' and result['is_valid']:\n+ await self.submit(form)\n+ return await self.json_response(result) \n \n+ async def submit(self,model=None):\n+ print(\"Submit sucess\")\ndiff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py\nindex 8099b01..8cb6567 100644\n--- a/src/snek/view/register_form.py\n+++ b/src/snek/view/register_form.py\n@@ -2,4 +2,8 @@ from snek.form.register import RegisterForm\n from snek.system.view import BaseFormView\n \n class RegisterFormView(BaseFormView):\n- form = RegisterForm\n\\ No newline at end of file\n+ form = RegisterForm\n+\n+ async def submit(self, form):\n+ result = await self.app.services.user.register(form.email.value,form.username.value,form.password.value)\n+ print(\"SUBMITTED:\",result)\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Added documentation subapp and markdown extension", "commit": "18b76ebd5e2f11451db04800d426a16b1ef1dd14", "diff": "commit 18b76ebd5e2f11451db04800d426a16b1ef1dd14\nAuthor: retoor \nDate: Fri Jan 24 23:33:36 2025 +0100\n\n Progress.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 0cca574..78c0e2b 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -2,11 +2,12 @@ import pathlib\n \n from aiohttp import web\n from app.app import Application as BaseApplication\n+from snek.docs.app import Application as DocsApplication\n from app.cache import time_cache_async\n-from jinja_markdown2 import MarkdownExtension\n from snek.mapper import get_mappers\n from snek.service import get_services\n from snek.system import http\n+from snek.system.markdown import MarkdownExtension\n from snek.system.middleware import cors_middleware\n from snek.view.about import AboutHTMLView, AboutMDView\n from snek.view.docs import DocsHTMLView, DocsMDView\n@@ -59,6 +60,8 @@ class Application(BaseApplication):\n self.router.add_get(\"/http-get\", self.handle_http_get)\n self.router.add_get(\"/http-photo\", self.handle_http_photo)\n \n+ self.add_subapp(\"/docs\", DocsApplication(path=pathlib.Path(__file__).parent.joinpath(\"docs\")))\n+\n async def handle_test(self, request):\n \n return await self.render_template(\ndiff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py\nindex 489a3e5..fcffe40 100644\n--- a/src/snek/system/markdown.py\n+++ b/src/snek/system/markdown.py\n@@ -1,6 +1,7 @@\n \n \n+from types import SimpleNamespace\n from mistune import escape\n from mistune import Markdown\n from mistune import HTMLRenderer\n@@ -12,6 +13,8 @@ import functools\n from app.cache import time_cache_async\n \n class MarkdownRenderer(HTMLRenderer):\n+\n+ _allow_harmful_protocols = True\n def __init__(self, app, template):\n self.template = template\n \n@@ -45,4 +48,29 @@ def render_markdown_sync(app, markdown_string):\n \n @time_cache_async(120)\n async def render_markdown(app, markdown_string):\n- return render_markdown_sync(app,markdown_string)\n\\ No newline at end of file\n+ return render_markdown_sync(app,markdown_string)\n+\n+from jinja2 import nodes, TemplateSyntaxError\n+from jinja2.ext import Extension\n+from jinja2.nodes import Const\n+\n+class MarkdownExtension(Extension):\n+ tags = {'markdown'}\n+\n+ def __init__(self, environment):\n+ self.app = SimpleNamespace(jinja2_env=environment)\n+ super(MarkdownExtension, self).__init__(environment)\n+\n+ def parse(self, parser):\n+ line_number = next(parser.stream).lineno\n+ md_file = [Const('')]\n+ body = ''\n+ try:\n+ md_file = [parser.parse_expression()]\n+ except TemplateSyntaxError:\n+ body = parser.parse_statements(['name:endmarkdown'], drop_needle=True)\n+ return nodes.CallBlock(self.call_method('_to_html', md_file), [], [], body).set_lineno(line_number)\n+\n+ def _to_html(self, md_file, caller):\n+ return render_markdown_sync(self.app,caller())\n\\ No newline at end of file\ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nindex ad02a6a..c6d57df 100644\n--- a/src/snek/templates/index.html\n+++ b/src/snek/templates/index.html\n@@ -16,7 +16,7 @@\n \n Design choices\n App preview\n- API docs\n+ API docs\n \n \n "} +{"repo": ".", "date": "2025-01-24", "line": "feat: Refactor form validation and rendering for improved consistency", "commit": "9b93403a93ac0b03a57fb5dc10db5c35349c4d6f", "diff": "commit 9b93403a93ac0b03a57fb5dc10db5c35349c4d6f\nAuthor: retoor \nDate: Fri Jan 24 23:35:44 2025 +0100\n\n Formatting.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 78c0e2b..ab19f42 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -1,9 +1,10 @@\n import pathlib\n+from types import SimpleNamespace\n \n from aiohttp import web\n from app.app import Application as BaseApplication\n+\n from snek.docs.app import Application as DocsApplication\n-from app.cache import time_cache_async\n from snek.mapper import get_mappers\n from snek.service import get_services\n from snek.system import http\n@@ -17,7 +18,6 @@ from snek.view.login_form import LoginFormView\n from snek.view.register import RegisterView\n from snek.view.register_form import RegisterFormView\n from snek.view.web import WebView\n-from types import SimpleNamespace\n \n \n class Application(BaseApplication):\n@@ -51,7 +51,7 @@ class Application(BaseApplication):\n self.router.add_view(\"/about.md\", AboutMDView)\n self.router.add_view(\"/docs.html\", DocsHTMLView)\n self.router.add_view(\"/docs.md\", DocsMDView)\n- \n+\n self.router.add_view(\"/web.html\", WebView)\n self.router.add_view(\"/login.html\", LoginView)\n self.router.add_view(\"/login.json\", LoginFormView)\n@@ -60,7 +60,10 @@ class Application(BaseApplication):\n self.router.add_get(\"/http-get\", self.handle_http_get)\n self.router.add_get(\"/http-photo\", self.handle_http_photo)\n \n- self.add_subapp(\"/docs\", DocsApplication(path=pathlib.Path(__file__).parent.joinpath(\"docs\")))\n+ self.add_subapp(\n+ \"/docs\",\n+ DocsApplication(path=pathlib.Path(__file__).parent.joinpath(\"docs\")),\n+ )\n \n async def handle_test(self, request):\n \n@@ -79,9 +82,8 @@ class Application(BaseApplication):\n return web.Response(\n body=path.read_bytes(), headers={\"Content-Type\": \"image/png\"}\n )\n- \n- \n+\n async def render_template(self, template, request, context=None):\n return await super().render_template(template, request, context)\n \ndiff --git a/src/snek/form/login.py b/src/snek/form/login.py\nindex 0d97d41..3d6d9a7 100644\n--- a/src/snek/form/login.py\n+++ b/src/snek/form/login.py\n@@ -1,24 +1,27 @@\n-from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement\n+from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement\n+\n \n class LoginForm(Form):\n \n title = HTMLElement(tag=\"h1\", text=\"Login\")\n \n username = FormInputElement(\n- name=\"username\", \n+ name=\"username\",\n required=True,\n min_length=2,\n max_length=20,\n regex=r\"^[a-zA-Z0-9_]+$\",\n place_holder=\"Username\",\n- type=\"text\"\n+ type=\"text\",\n+ )\n+ password = FormInputElement(\n+ name=\"password\",\n+ required=True,\n+ regex=r\"^[a-zA-Z0-9_.+-]{6,}\",\n+ type=\"password\",\n+ place_holder=\"Password\",\n )\n- password = FormInputElement(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\",type=\"password\",place_holder=\"Password\")\n \n action = FormButtonElement(\n- name=\"action\",\n- value=\"submit\",\n- text=\"Login\",\n- type=\"button\"\n+ name=\"action\", value=\"submit\", text=\"Login\", type=\"button\"\n )\n- \ndiff --git a/src/snek/form/register.py b/src/snek/form/register.py\nindex 4252bf1..1384b8f 100644\n--- a/src/snek/form/register.py\n+++ b/src/snek/form/register.py\n@@ -1,4 +1,5 @@\n-from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement\n+from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement\n+\n \n class UsernameField(FormInputElement):\n \n@@ -9,32 +10,35 @@ class UsernameField(FormInputElement):\n result.append(\"Username is not available.\")\n return result\n \n+\n class RegisterForm(Form):\n \n title = HTMLElement(tag=\"h1\", text=\"Register\")\n \n username = UsernameField(\n- name=\"username\", \n+ name=\"username\",\n required=True,\n min_length=2,\n max_length=20,\n regex=r\"^[a-zA-Z0-9_]+$\",\n place_holder=\"Username\",\n- type=\"text\"\n+ type=\"text\",\n )\n email = FormInputElement(\n name=\"email\",\n required=False,\n regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n place_holder=\"Email address\",\n- type=\"email\"\n+ type=\"email\",\n+ )\n+ password = FormInputElement(\n+ name=\"password\",\n+ required=True,\n+ regex=r\"^[a-zA-Z0-9_.+-]{6,}\",\n+ type=\"password\",\n+ place_holder=\"Password\",\n )\n- password = FormInputElement(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\",type=\"password\",place_holder=\"Password\")\n \n action = FormButtonElement(\n- name=\"action\",\n- value=\"submit\",\n- text=\"Register\",\n- type=\"button\"\n+ name=\"action\", value=\"submit\", text=\"Register\", type=\"button\"\n )\n-\ndiff --git a/src/snek/mapper/__init__.py b/src/snek/mapper/__init__.py\nindex dc9e047..2b9b79f 100644\n--- a/src/snek/mapper/__init__.py\n+++ b/src/snek/mapper/__init__.py\n@@ -1,12 +1,12 @@\n-import functools \n+import functools\n+\n from snek.mapper.user import UserMapper\n \n-@functools.cache \n+\n+@functools.cache\n def get_mappers(app=None):\n- return dict(\n- user=UserMapper(app=app)\n+ return {\"user\": UserMapper(app=app)}\n \n- )\n \n def get_mapper(name, app=None):\n- return get_mappers(app=app)[name]\n\\ No newline at end of file\n+ return get_mappers(app=app)[name]\ndiff --git a/src/snek/mapper/user.py b/src/snek/mapper/user.py\nindex 642028c..c388abc 100644\n--- a/src/snek/mapper/user.py\n+++ b/src/snek/mapper/user.py\n@@ -1,6 +1,7 @@\n-from snek.system.mapper import BaseMapper\n from snek.model.user import UserModel\n+from snek.system.mapper import BaseMapper\n+\n \n class UserMapper(BaseMapper):\n table_name = \"user\"\n- model_class = UserModel \n\\ No newline at end of file\n+ model_class = UserModel\ndiff --git a/src/snek/model/__init__.py b/src/snek/model/__init__.py\nindex 52af21a..081ae15 100644\n--- a/src/snek/model/__init__.py\n+++ b/src/snek/model/__init__.py\n@@ -1,12 +1,12 @@\n-from snek.model.user import UserModel \n-import functools \n+import functools\n+\n+from snek.model.user import UserModel\n+\n \n @functools.cache\n def get_models():\n- return dict(\n- user=UserModel\n+ return {\"user\": UserModel}\n \n- )\n \n def get_model(name):\n return get_models()[name]\ndiff --git a/src/snek/model/user.py b/src/snek/model/user.py\nindex 254b6c9..adb236b 100644\n--- a/src/snek/model/user.py\n+++ b/src/snek/model/user.py\n@@ -1,9 +1,10 @@\n-from snek.system.model import BaseModel,ModelField\n+from snek.system.model import BaseModel, ModelField\n+\n \n class UserModel(BaseModel):\n- \n+\n username = ModelField(\n- name=\"username\", \n+ name=\"username\",\n required=True,\n min_length=2,\n max_length=20,\n@@ -12,8 +13,6 @@ class UserModel(BaseModel):\n email = ModelField(\n name=\"email\",\n required=False,\n- regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\"\n+ regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n )\n- password = ModelField(name=\"password\",required=True,regex=r\"^[a-zA-Z0-9_.+-]{6,}\")\n-\n-\n+ password = ModelField(name=\"password\", required=True, regex=r\"^[a-zA-Z0-9_.+-]{6,}\")\ndiff --git a/src/snek/service/__init__.py b/src/snek/service/__init__.py\nindex 4038f70..60fec76 100644\n--- a/src/snek/service/__init__.py\n+++ b/src/snek/service/__init__.py\n@@ -1,12 +1,13 @@\n-from snek.service.user import UserService \n-import functools \n+import functools\n+\n+from snek.service.user import UserService\n+\n \n @functools.cache\n def get_services(app):\n \n- return dict(\n- user = UserService(app=app)\n+ return {\"user\": UserService(app=app)}\n+\n \n- )\n def get_service(name, app=None):\n- return get_services(app=app)[name]\n\\ No newline at end of file\n+ return get_services(app=app)[name]\ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nindex a2b0cb1..5124640 100644\n--- a/src/snek/service/user.py\n+++ b/src/snek/service/user.py\n@@ -1,5 +1,6 @@\n-from snek.system.service import BaseService \n-from snek.system import security \n+from snek.system import security\n+from snek.system.service import BaseService\n+\n \n class UserService(BaseService):\n mapper_name = \"user\"\n@@ -12,6 +13,5 @@ class UserService(BaseService):\n model.username = username\n model.password = await security.hash(password)\n if await self.save(model):\n- return model \n+ return model\n raise Exception(f\"Failed to create user: {model.errors}.\")\n- \n\\ No newline at end of file\ndiff --git a/src/snek/system/cache.py b/src/snek/system/cache.py\nindex 2992803..5e275d9 100644\n--- a/src/snek/system/cache.py\n+++ b/src/snek/system/cache.py\n@@ -1,8 +1,8 @@\n-\n-import functools \n+import functools\n \n cache = functools.cache\n \n+\n def async_cache(func):\n cache = {}\n \n@@ -14,4 +14,4 @@ def async_cache(func):\n cache[args] = result\n return result\n \n- return wrapper\n\\ No newline at end of file\n+ return wrapper\ndiff --git a/src/snek/system/form.py b/src/snek/system/form.py\nindex 82091b6..f4cf2d3 100644\n--- a/src/snek/system/form.py\n+++ b/src/snek/system/form.py\n@@ -5,17 +5,17 @@\n \n@@ -26,8 +26,19 @@\n \n from snek.system import model\n \n+\n class HTMLElement(model.ModelField):\n- def __init__(self, id=None, tag=\"div\", name=None, html=None, class_name=None, text=None, *args, **kwargs):\n+ def __init__(\n+ self,\n+ id=None,\n+ tag=\"div\",\n+ name=None,\n+ html=None,\n+ class_name=None,\n+ text=None,\n+ *args,\n+ **kwargs,\n+ ):\n self.tag = tag\n self.text = text\n self.id = id\n@@ -37,16 +48,18 @@ class HTMLElement(model.ModelField):\n \n async def to_json(self):\n result = await super().to_json()\n- result['text'] = self.text\n- result['id'] = self.id\n- result['html'] = self.html\n- result['class_name'] = self.class_name\n- result['tag'] = self.tag\n+ result[\"text\"] = self.text\n+ result[\"id\"] = self.id\n+ result[\"html\"] = self.html\n+ result[\"class_name\"] = self.class_name\n+ result[\"tag\"] = self.tag\n return result\n \n+\n class FormElement(HTMLElement):\n pass\n \n+\n class FormInputElement(FormElement):\n def __init__(self, type=\"text\", place_holder=None, *args, **kwargs):\n super().__init__(tag=\"input\", *args, **kwargs)\n@@ -59,25 +72,27 @@ class FormInputElement(FormElement):\n data[\"type\"] = self.type\n return data\n \n+\n class FormButtonElement(FormElement):\n def __init__(self, tag=\"button\", *args, **kwargs):\n super().__init__(tag=tag, *args, **kwargs)\n \n+\n class Form(model.BaseModel):\n @property\n def html_elements(self):\n return [element for element in self.fields if isinstance(element, HTMLElement)]\n \n def set_user_data(self, data):\n- return super().set_user_data(data.get('fields'))\n+ return super().set_user_data(data.get(\"fields\"))\n \n async def to_json(self, encode=False):\n elements = await super().to_json()\n html_elements = {}\n for element in elements.keys():\n- if element == 'is_valid':\n+ if element == \"is_valid\":\n- continue \n+ continue\n field = getattr(self, element)\n if isinstance(field, HTMLElement):\n try:\n@@ -85,8 +100,12 @@ class Form(model.BaseModel):\n except KeyError:\n pass\n \n- is_valid = all(field['is_valid'] for field in html_elements.values())\n- return dict(fields=html_elements, is_valid=is_valid, errors=await self.errors)\n+ is_valid = all(field[\"is_valid\"] for field in html_elements.values())\n+ return {\n+ \"fields\": html_elements,\n+ \"is_valid\": is_valid,\n+ \"errors\": await self.errors,\n+ }\n \n @property\n async def errors(self):\n@@ -98,4 +117,4 @@ class Form(model.BaseModel):\n @property\n async def is_valid(self):\n- return False\n\\ No newline at end of file\n+ return False\ndiff --git a/src/snek/system/http.py b/src/snek/system/http.py\nindex b5e8b4f..cd8a9b1 100644\n--- a/src/snek/system/http.py\n+++ b/src/snek/system/http.py\n@@ -11,10 +11,10 @@\n@@ -24,17 +24,17 @@\n \n \n-from aiohttp import web\n-import aiohttp\n-from app.cache import time_cache_async\n-from bs4 import BeautifulSoup\n-from urllib.parse import urljoin\n+import asyncio\n import pathlib\n import uuid\n-import imgkit\n-import asyncio\n import zlib\n-import io\n+from urllib.parse import urljoin\n+\n+import aiohttp\n+import imgkit\n+from app.cache import time_cache_async\n+from bs4 import BeautifulSoup\n+\n \n async def crc32(data):\n try:\n@@ -43,6 +43,7 @@ async def crc32(data):\n pass\n return \"crc32\" + str(zlib.crc32(data))\n \n+\n async def get_file(name, suffix=\".cache\"):\n name = await crc32(name)\n path = pathlib.Path(\".\").joinpath(\"cache\")\n@@ -50,17 +51,19 @@ async def get_file(name, suffix=\".cache\"):\n path.mkdir(parents=True, exist_ok=True)\n return path.joinpath(name + suffix)\n \n+\n async def public_touch(name=None):\n path = pathlib.Path(\".\").joinpath(str(uuid.uuid4()) + name)\n path.open(\"wb\").close()\n return path\n \n+\n async def create_site_photo(url):\n loop = asyncio.get_event_loop()\n if not url.startswith(\"https\"):\n output_path = await get_file(\"site-screenshot-\" + url, \".png\")\n- \n+\n if output_path.exists():\n return output_path\n output_path.touch()\n@@ -71,21 +74,23 @@ async def create_site_photo(url):\n \n return await loop.run_in_executor(None, make_photo)\n \n+\n async def repair_links(base_url, html_content):\n soup = BeautifulSoup(html_content, \"html.parser\")\n- for tag in soup.find_all(['a', 'img', 'link']):\n- if tag.has_attr('href') and not tag['href'].startswith(\"http\"):\n- tag['href'] = urljoin(base_url, tag['href'])\n- if tag.has_attr('src') and not tag['src'].startswith(\"http\"):\n- tag['src'] = urljoin(base_url, tag['src'])\n+ for tag in soup.find_all([\"a\", \"img\", \"link\"]):\n+ if tag.has_attr(\"href\") and not tag[\"href\"].startswith(\"http\"):\n+ tag[\"href\"] = urljoin(base_url, tag[\"href\"])\n+ if tag.has_attr(\"src\") and not tag[\"src\"].startswith(\"http\"):\n+ tag[\"src\"] = urljoin(base_url, tag[\"src\"])\n return soup.prettify()\n \n+\n async def is_html_content(content: bytes):\n try:\n- content = content.decode(errors='ignore')\n+ content = content.decode(errors=\"ignore\")\n except:\n pass\n- marks = [' BaseModel:\n+ async def get(self, uid: str = None, **kwargs) -> BaseModel:\n if uid:\n- kwargs['uid'] = uid \n- model = self.new()\n+ kwargs[\"uid\"] = uid\n+ self.new()\n record = self.table.find_one(**kwargs)\n- return await self.model_class.from_record(mapper=self,record=record)\n+ return await self.model_class.from_record(mapper=self, record=record)\n \n async def exists(self, **kwargs):\n return self.table.exists(**kwargs)\n@@ -40,19 +39,19 @@ class BaseMapper:\n async def count(self, **kwargs) -> int:\n return self.table.count(**kwargs)\n \n- async def save(self, model:BaseModel) -> bool:\n+ async def save(self, model: BaseModel) -> bool:\n record = await model.record\n- if not record.get('uid'):\n+ if not record.get(\"uid\"):\n raise Exception(f\"Attempt to save without uid: {record}.\")\n- return self.table.upsert(record,['uid'])\n+ return self.table.upsert(record, [\"uid\"])\n \n async def find(self, **kwargs) -> typing.AsyncGenerator:\n if not kwargs.get(\"_limit\"):\n kwargs[\"_limit\"] = self.default_limit\n for record in self.table.find(**kwargs):\n- yield await self.model_class.from_record(mapper=self,record=record)\n- \n- async def delete(self, kwargs=None)-> int: \n+ yield await self.model_class.from_record(mapper=self, record=record)\n+\n+ async def delete(self, kwargs=None) -> int:\n if not kwargs or not isinstance(kwargs, dict):\n raise Exception(\"Can't execute delete with no filter.\")\n return self.table.delete(**kwargs)\ndiff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py\nindex fcffe40..23d0656 100644\n--- a/src/snek/system/markdown.py\n+++ b/src/snek/system/markdown.py\n@@ -1,62 +1,65 @@\n-\n \n from types import SimpleNamespace\n-from mistune import escape\n-from mistune import Markdown\n-from mistune import HTMLRenderer\n+\n+from app.cache import time_cache_async\n+from mistune import HTMLRenderer, Markdown\n from pygments import highlight\n-from pygments.lexers import get_lexer_by_name\n from pygments.formatters import html\n-from pygments.styles import get_style_by_name\n-import functools\n-from app.cache import time_cache_async\n+from pygments.lexers import get_lexer_by_name\n+\n \n class MarkdownRenderer(HTMLRenderer):\n \n _allow_harmful_protocols = True\n+\n def __init__(self, app, template):\n- self.template = template\n- \n- self.app = app\n- self.env = self.app.jinja2_env\n- formatter = html.HtmlFormatter()\n- self.env.globals['highlight_styles'] = formatter.get_style_defs()\n- def _escape(self,str):\n- def block_code(self, code, lang=None,info=None):\n+ self.template = template\n+\n+ self.app = app\n+ self.env = self.app.jinja2_env\n+ formatter = html.HtmlFormatter()\n+ self.env.globals[\"highlight_styles\"] = formatter.get_style_defs()\n+\n+ def _escape(self, str):\n+\n+ def block_code(self, code, lang=None, info=None):\n if not lang:\n lang = info\n if not lang:\n return f\"
{code}
\"\n lexer = get_lexer_by_name(lang, stripall=True)\n formatter = html.HtmlFormatter(lineseparator=\"
\")\n return highlight(code, lexer, formatter)\n+\n def render(self):\n markdown_string = self.app.template_path.joinpath(self.template).read_text()\n- renderer = MarkdownRenderer(self.app,self.template)\n+ renderer = MarkdownRenderer(self.app, self.template)\n markdown = Markdown(renderer=renderer)\n return markdown(markdown_string)\n \n \n-\n def render_markdown_sync(app, markdown_string):\n- renderer = MarkdownRenderer(app,None)\n+ renderer = MarkdownRenderer(app, None)\n markdown = Markdown(renderer=renderer)\n return markdown(markdown_string)\n \n+\n @time_cache_async(120)\n async def render_markdown(app, markdown_string):\n- return render_markdown_sync(app,markdown_string)\n+ return render_markdown_sync(app, markdown_string)\n \n-from jinja2 import nodes, TemplateSyntaxError\n+\n+from jinja2 import TemplateSyntaxError, nodes\n from jinja2.ext import Extension\n from jinja2.nodes import Const\n \n+\n class MarkdownExtension(Extension):\n- tags = {'markdown'}\n+ tags = {\"markdown\"}\n \n def __init__(self, environment):\n self.app = SimpleNamespace(jinja2_env=environment)\n@@ -64,13 +67,15 @@ class MarkdownExtension(Extension):\n \n def parse(self, parser):\n line_number = next(parser.stream).lineno\n- md_file = [Const('')]\n- body = ''\n+ md_file = [Const(\"\")]\n+ body = \"\"\n try:\n md_file = [parser.parse_expression()]\n except TemplateSyntaxError:\n- body = parser.parse_statements(['name:endmarkdown'], drop_needle=True)\n- return nodes.CallBlock(self.call_method('_to_html', md_file), [], [], body).set_lineno(line_number)\n+ body = parser.parse_statements([\"name:endmarkdown\"], drop_needle=True)\n+ return nodes.CallBlock(\n+ self.call_method(\"_to_html\", md_file), [], [], body\n+ ).set_lineno(line_number)\n \n def _to_html(self, md_file, caller):\n- return render_markdown_sync(self.app,caller())\n\\ No newline at end of file\n+ return render_markdown_sync(self.app, caller())\ndiff --git a/src/snek/system/middleware.py b/src/snek/system/middleware.py\nindex 7fe457f..69fe378 100644\n--- a/src/snek/system/middleware.py\n+++ b/src/snek/system/middleware.py\n@@ -8,12 +8,14 @@\n \n from aiohttp import web\n \n+\n @web.middleware\n async def no_cors_middleware(request, handler):\n response = await handler(request)\n response.headers.pop(\"Access-Control-Allow-Origin\", None)\n return response\n \n+\n @web.middleware\n async def cors_allow_middleware(request, handler):\n response = await handler(request)\n@@ -22,12 +24,15 @@ async def cors_allow_middleware(request, handler):\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n return response\n \n+\n @web.middleware\n async def cors_middleware(request, handler):\n if request.method == \"OPTIONS\":\n response = web.Response()\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n- response.headers[\"Access-Control-Allow-Methods\"] = \"GET, POST, PUT, DELETE, OPTIONS\"\n+ response.headers[\"Access-Control-Allow-Methods\"] = (\n+ \"GET, POST, PUT, DELETE, OPTIONS\"\n+ )\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n return response\n \n@@ -35,4 +40,4 @@ async def cors_middleware(request, handler):\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n response.headers[\"Access-Control-Allow-Methods\"] = \"GET, POST, PUT, DELETE, OPTIONS\"\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n- return response\n\\ No newline at end of file\n+ return response\ndiff --git a/src/snek/system/model.py b/src/snek/system/model.py\nindex 0d700ff..b41f4ba 100644\n--- a/src/snek/system/model.py\n+++ b/src/snek/system/model.py\n@@ -25,12 +25,12 @@\n \n \n+import copy\n+import json\n import re\n import uuid\n-import json\n-from datetime import datetime, timezone\n from collections import OrderedDict\n-import copy\n+from datetime import datetime, timezone\n \n TIMESTAMP_REGEX = r\"^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}\\+\\d{2}:\\d{2}$\"\n \n@@ -44,12 +44,21 @@ def add_attrs(**kwargs):\n for key, value in kwargs.items():\n setattr(func, key, value)\n return func\n+\n return decorator\n \n \n-def validate_attrs(required=False, min_length=None, max_length=None, regex=None, **kwargs):\n+def validate_attrs(\n+ required=False, min_length=None, max_length=None, regex=None, **kwargs\n+):\n def decorator(func):\n- return add_attrs(required=required, min_length=min_length, max_length=max_length, regex=regex, **kwargs)(func)\n+ return add_attrs(\n+ required=required,\n+ min_length=min_length,\n+ max_length=max_length,\n+ regex=regex,\n+ **kwargs,\n+ )(func)\n \n \n class Validator:\n@@ -70,7 +79,21 @@ class Validator:\n def custom_validation(self):\n return True\n \n- def __init__(self, required=False, min_num=None, max_num=None, min_length=None, max_length=None, regex=None, value=None, kind=None, help_text=None, app=None, model=None, **kwargs):\n+ def __init__(\n+ self,\n+ required=False,\n+ min_num=None,\n+ max_num=None,\n+ min_length=None,\n+ max_length=None,\n+ regex=None,\n+ value=None,\n+ kind=None,\n+ help_text=None,\n+ app=None,\n+ model=None,\n+ **kwargs,\n+ ):\n self.index = Validator._index\n Validator._index += 1\n self.app = app\n@@ -103,9 +126,13 @@ class Validator:\n if self.max_num is not None and self.value > self.max_num:\n error_list.append(f\"Field should be maximal {self.max_num}.\")\n if self.min_length is not None and len(self.value) < self.min_length:\n- error_list.append(f\"Field should be minimal {self.min_length} characters long.\")\n+ error_list.append(\n+ f\"Field should be minimal {self.min_length} characters long.\"\n+ )\n if self.max_length is not None and len(self.value) > self.max_length:\n- error_list.append(f\"Field should be maximal {self.max_length} characters long.\")\n+ error_list.append(\n+ f\"Field should be maximal {self.max_length} characters long.\"\n+ )\n if self.regex and self.value and not re.match(self.regex, self.value):\n error_list.append(\"Invalid value.\")\n if self.kind and not isinstance(self.value, self.kind):\n@@ -141,7 +168,7 @@ class Validator:\n \"help_text\": self.help_text,\n \"errors\": errors,\n \"is_valid\": is_valid,\n- \"index\": self.index\n+ \"index\": self.index,\n }\n \n \n@@ -156,7 +183,7 @@ class ModelField(Validator):\n \n async def to_json(self):\n result = await super().to_json()\n- result['name'] = self.name\n+ result[\"name\"] = self.name\n return result\n \n \n@@ -193,30 +220,39 @@ class UUIDField(ModelField):\n class BaseModel:\n \n uid = UUIDField(name=\"uid\", required=True)\n- created_at = CreatedField(name=\"created_at\", required=True, regex=TIMESTAMP_REGEX, place_holder=\"Created at\")\n- updated_at = UpdatedField(name=\"updated_at\", regex=TIMESTAMP_REGEX, place_holder=\"Updated at\")\n- deleted_at = DeletedField(name=\"deleted_at\", regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\")\n-\n- @classmethod \n+ created_at = CreatedField(\n+ name=\"created_at\",\n+ required=True,\n+ regex=TIMESTAMP_REGEX,\n+ place_holder=\"Created at\",\n+ )\n+ updated_at = UpdatedField(\n+ name=\"updated_at\", regex=TIMESTAMP_REGEX, place_holder=\"Updated at\"\n+ )\n+ deleted_at = DeletedField(\n+ name=\"deleted_at\", regex=TIMESTAMP_REGEX, place_holder=\"Deleted at\"\n+ )\n+\n+ @classmethod\n async def from_record(cls, record, mapper):\n model = cls.__new__()\n- model.mapper = mapper \n+ model.mapper = mapper\n model.record = record\n return model\n \n- @property \n+ @property\n def mapper(self):\n- return self._mapper \n+ return self._mapper\n \n- @mapper.setter \n+ @mapper.setter\n def mapper(self, value):\n- self._mapper = value \n+ self._mapper = value\n \n- @property \n+ @property\n def record(self):\n return {field.name: field.value for field in self.fields}\n- \n- @record.setter \n+\n+ @record.setter\n def record(self, value):\n for key, value in self._record.items():\n field = self.fields.get(key)\n@@ -233,10 +269,12 @@ class BaseModel:\n \n if isinstance(obj, Validator):\n self.__dict__[key] = copy.deepcopy(obj)\n- self.__dict__[key].value = kwargs.pop(key, self.__dict__[key].initial_value)\n+ self.__dict__[key].value = kwargs.pop(\n+ key, self.__dict__[key].initial_value\n+ )\n self.fields[key] = self.__dict__[key]\n self.fields[key].model = self\n- self.fields[key].app = kwargs.get('app')\n+ self.fields[key].app = kwargs.get(\"app\")\n \n def __setitem__(self, key, value):\n obj = self.__dict__.get(key)\n@@ -254,16 +292,13 @@ class BaseModel:\n field = self.fields.get(key)\n if not field:\n continue\n- if value.get('name'):\n- value = value.get('value')\n+ if value.get(\"name\"):\n+ value = value.get(\"value\")\n field.value = value\n \n- \n-\n @property\n async def is_valid(self):\n return all([await field.is_valid for field in self.fields.values()])\n- \n \n def __getitem__(self, key):\n obj = self.__dict__.get(key)\n@@ -282,20 +317,22 @@ class BaseModel:\n obj = await self.to_json()\n record = {}\n for key, value in obj.items():\n- if not isinstance(value, dict) or not 'value' in value:\n+ if not isinstance(value, dict) or \"value\" not in value:\n continue\n if getattr(self, key).save:\n- record[key] = value.get('value')\n+ record[key] = value.get(\"value\")\n return record\n \n async def to_json(self, encode=False):\n- model_data = OrderedDict({\n- \"uid\": self.uid.value,\n- \"created_at\": self.created_at.value,\n- \"updated_at\": self.updated_at.value,\n- \"deleted_at\": self.deleted_at.value,\n- \"is_valid\": await self.is_valid\n- })\n+ model_data = OrderedDict(\n+ {\n+ \"uid\": self.uid.value,\n+ \"created_at\": self.created_at.value,\n+ \"updated_at\": self.updated_at.value,\n+ \"deleted_at\": self.deleted_at.value,\n+ \"is_valid\": await self.is_valid,\n+ }\n+ )\n \n for key, value in self.fields.items():\n if key == \"record\":\n@@ -325,4 +362,4 @@ class FormElement(ModelField):\n data = await super().to_json()\n data[\"name\"] = self.name\n data[\"place_holder\"] = self.place_holder\n- return data\n\\ No newline at end of file\n+ return data\ndiff --git a/src/snek/system/security.py b/src/snek/system/security.py\nindex b319f54..5449c50 100644\n--- a/src/snek/system/security.py\n+++ b/src/snek/system/security.py\n@@ -1,12 +1,13 @@\n-import hashlib \n+import hashlib\n \n DEFAULT_SALT = b\"snekker-de-snek-\"\n \n-async def hash(data,salt=DEFAULT_SALT):\n+\n+async def hash(data, salt=DEFAULT_SALT):\n try:\n data = data.encode(errors=\"ignore\")\n except AttributeError:\n- pass \n+ pass\n try:\n salt = salt.encode(errors=\"ignore\")\n except AttributeError:\n@@ -16,5 +17,6 @@ async def hash(data,salt=DEFAULT_SALT):\n obj = hashlib.sha256(salted)\n return obj.hexdigest()\n \n-async def verify(string:str, hashed:str):\n- return await hash(string) == hashed \n+\n+async def verify(string: str, hashed: str):\n+ return await hash(string) == hashed\ndiff --git a/src/snek/system/service.py b/src/snek/system/service.py\nindex d970b5f..1f9d601 100644\n--- a/src/snek/system/service.py\n+++ b/src/snek/system/service.py\n@@ -1,24 +1,22 @@\n-\n-\n-\n from snek.mapper import get_mapper\n-from snek.system.mapper import BaseMapper \n from snek.model.user import UserModel\n+from snek.system.mapper import BaseMapper\n+\n \n class BaseService:\n \n- mapper_name:BaseMapper = None\n+ mapper_name: BaseMapper = None\n \n def __init__(self, app):\n- self.app = app \n+ self.app = app\n if self.mapper_name:\n self.mapper = get_mapper(self.mapper_name, app=self.app)\n else:\n- self.mapper = None \n+ self.mapper = None\n \n async def exists(self, **kwargs):\n return await self.count(**kwargs) > 0\n- \n+\n async def count(self, **kwargs):\n return await self.mapper.count(**kwargs)\n \n@@ -27,14 +25,13 @@ class BaseService:\n \n async def get(self, **kwargs):\n return await self.mapper.get(**kwargs)\n- \n- async def save(self, model:UserModel):\n+\n+ async def save(self, model: UserModel):\n- return await self.mapper.save(model) and True \n- \n- \n+ return await self.mapper.save(model) and True\n+\n async def find(self, **kwargs):\n return await self.mapper.find(**kwargs)\n- \n+\n async def delete(self, **kwargs):\n- return await self.mapper.delete(**kwargs)\n\\ No newline at end of file\n+ return await self.mapper.delete(**kwargs)\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nindex 3b53f33..1cf5329 100644\n--- a/src/snek/system/view.py\n+++ b/src/snek/system/view.py\n@@ -1,13 +1,14 @@\n from aiohttp import web\n \n-from snek.system.markdown import render_markdown \n+from snek.system.markdown import render_markdown\n+\n \n class BaseView(web.View):\n- \n- @property \n+\n+ @property\n def app(self):\n return self.request.app\n- \n+\n @property\n def db(self):\n return self.app.db\n@@ -17,31 +18,36 @@ class BaseView(web.View):\n \n async def render_template(self, template_name, context=None):\n if template_name.endswith(\".md\"):\n- response = await self.request.app.render_template(template_name,self.request,context)\n+ response = await self.request.app.render_template(\n+ template_name, self.request, context\n+ )\n body = await render_markdown(self.app, response.body.decode())\n- return web.Response(body=body,content_type=\"text/html\")\n- return await self.request.app.render_template(template_name, self.request,context)\n- \n+ return web.Response(body=body, content_type=\"text/html\")\n+ return await self.request.app.render_template(\n+ template_name, self.request, context\n+ )\n+\n+\n class BaseFormView(BaseView):\n \n- form = None \n+ form = None\n \n async def get(self):\n form = self.form(app=self.app)\n- \n+\n return await self.json_response(await form.to_json())\n- \n+\n async def post(self):\n form = self.form(app=self.app)\n post = await self.request.json()\n- form.set_user_data(post['form'])\n+ form.set_user_data(post[\"form\"])\n result = await form.to_json()\n- if post.get('action') == 'validate':\n+ if post.get(\"action\") == \"validate\":\n pass\n- if post.get('action') == 'submit' and result['is_valid']:\n+ if post.get(\"action\") == \"submit\" and result[\"is_valid\"]:\n await self.submit(form)\n- return await self.json_response(result) \n+ return await self.json_response(result)\n \n- async def submit(self,model=None):\n+ async def submit(self, model=None):\n print(\"Submit sucess\")\ndiff --git a/src/snek/templates/docs.md b/src/snek/templates/docs.md\nindex 2cf60cb..a42b7d5 100644\n--- a/src/snek/templates/docs.md\n+++ b/src/snek/templates/docs.md\n@@ -9,8 +9,7 @@ Currently only some details about the internal API are available.\n \n new_user_object = await app.service.user.register(\n- username=\"retoor\", \n- password=\"retoorded\"\n+ username=\"retoor\", password=\"retoorded\"\n )\n ```\n \n@@ -23,15 +22,16 @@ var1 = security.encrypt(\"data\")\n var2 = security.encrypt(b\"data\")\n \n-assert(var1 == var2)\n+assert var1 == var2\n ```\n \n ```python\n-from snek.system.view import BaseView \n+from snek.system.view import BaseView\n+\n \n class IndexView(BaseView):\n- \n+\n async def get(self):\n@@ -40,11 +40,12 @@ class IndexView(BaseView):\n ```\n ```python\n-from snek.system.view import BaseFormView\n from snek.form.register import RegisterForm\n+from snek.system.view import BaseFormView\n+\n \n class RegisterFormView(BaseFormView):\n- \n+\n form = RegisterForm\n ```\ndiff --git a/src/snek/view/about.py b/src/snek/view/about.py\nindex 593d5a9..762fc8e 100644\n--- a/src/snek/view/about.py\n+++ b/src/snek/view/about.py\n@@ -1,5 +1,3 @@\n-\n-\n from snek.system.view import BaseView\n \n \n@@ -7,8 +5,9 @@ class AboutHTMLView(BaseView):\n \n async def get(self):\n return await self.render_template(\"about.html\")\n- \n+\n+\n class AboutMDView(BaseView):\n \n async def get(self):\n- return await self.render_template(\"about.md\")\n\\ No newline at end of file\n+ return await self.render_template(\"about.md\")\ndiff --git a/src/snek/view/docs.py b/src/snek/view/docs.py\nindex e69d754..519a0eb 100644\n--- a/src/snek/view/docs.py\n+++ b/src/snek/view/docs.py\n@@ -1,6 +1,3 @@\n-\n-\n-\n from snek.system.view import BaseView\n \n \n@@ -8,8 +5,9 @@ class DocsHTMLView(BaseView):\n \n async def get(self):\n return await self.render_template(\"docs.html\")\n- \n+\n+\n class DocsMDView(BaseView):\n \n async def get(self):\n- return await self.render_template(\"docs.md\")\n\\ No newline at end of file\n+ return await self.render_template(\"docs.md\")\ndiff --git a/src/snek/view/index.py b/src/snek/view/index.py\nindex c7861fa..bd91dc8 100644\n--- a/src/snek/view/index.py\n+++ b/src/snek/view/index.py\n@@ -1,5 +1,6 @@\n from snek.system.view import BaseView\n \n+\n class IndexView(BaseView):\n \n async def get(self):\ndiff --git a/src/snek/view/login.py b/src/snek/view/login.py\nindex ffedc79..6566df9 100644\n--- a/src/snek/view/login.py\n+++ b/src/snek/view/login.py\n@@ -1,13 +1,18 @@\n from snek.form.register import RegisterForm\n from snek.system.view import BaseView\n \n+\n class LoginView(BaseView):\n \n async def get(self):\n- \n+ return await self.render_template(\n+ \"login.html\"\n+\n async def post(self):\n form = RegisterForm()\n form.set_user_data(await self.request.post())\n print(form.is_valid())\n+ return await self.render_template(\n+ \"login.html\", self.request\ndiff --git a/src/snek/view/login_form.py b/src/snek/view/login_form.py\nindex e9b6eac..576ddc6 100644\n--- a/src/snek/view/login_form.py\n+++ b/src/snek/view/login_form.py\n@@ -1,5 +1,6 @@\n-from snek.system.view import BaseFormView\n from snek.form.login import LoginForm\n+from snek.system.view import BaseFormView\n+\n \n class LoginFormView(BaseFormView):\n- form = LoginForm\n\\ No newline at end of file\n+ form = LoginForm\ndiff --git a/src/snek/view/register.py b/src/snek/view/register.py\nindex e3b3038..1186959 100644\n--- a/src/snek/view/register.py\n+++ b/src/snek/view/register.py\n@@ -1,6 +1,7 @@\n from snek.system.view import BaseView\n \n+\n class RegisterView(BaseView):\n \n async def get(self):\n- return await self.render_template(\"register.html\") \n\\ No newline at end of file\n+ return await self.render_template(\"register.html\")\ndiff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py\nindex 8cb6567..4c30169 100644\n--- a/src/snek/view/register_form.py\n+++ b/src/snek/view/register_form.py\n@@ -1,9 +1,12 @@\n from snek.form.register import RegisterForm\n from snek.system.view import BaseFormView\n \n+\n class RegisterFormView(BaseFormView):\n form = RegisterForm\n \n async def submit(self, form):\n- result = await self.app.services.user.register(form.email.value,form.username.value,form.password.value)\n- print(\"SUBMITTED:\",result)\n\\ No newline at end of file\n+ result = await self.app.services.user.register(\n+ form.email.value, form.username.value, form.password.value\n+ )\n+ print(\"SUBMITTED:\", result)\ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex b06563a..d42fcec 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -1,6 +1,7 @@\n from snek.system.view import BaseView\n \n+\n class WebView(BaseView):\n \n async def get(self):\n- return await self.render_template(\"web.html\")\n\\ No newline at end of file\n+ return await self.render_template(\"web.html\")"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Added initial documentation for the Snek project.", "commit": "b56371994f5d3f5c1aa5d63c28efd18856ea8e9b", "diff": "commit b56371994f5d3f5c1aa5d63c28efd18856ea8e9b\nAuthor: retoor \nDate: Fri Jan 24 23:41:54 2025 +0100\n\n Added docs.\n\ndiff --git a/src/snek/docs/__pycache__/app.cpython-312.pyc b/src/snek/docs/__pycache__/app.cpython-312.pyc\nnew file mode 100644\nindex 0000000..087bb64\nBinary files /dev/null and b/src/snek/docs/__pycache__/app.cpython-312.pyc differ\ndiff --git a/src/snek/docs/app.py b/src/snek/docs/app.py\nnew file mode 100644\nindex 0000000..5e00d98\n--- /dev/null\n+++ b/src/snek/docs/app.py\n@@ -0,0 +1,33 @@\n+from app.app import Application as BaseApplication\n+import pathlib \n+from aiohttp import web\n+from snek.system.markdown import MarkdownExtension\n+\n+from snek.system.markdown import render_markdown \n+\n+class Application(BaseApplication):\n+\n+ def __init__(self, path=None, *args,**kwargs):\n+ self.path = pathlib.Path(path)\n+ template_path = self.path\n+\n+ super().__init__(template_path=template_path ,*args, **kwargs)\n+ self.jinja2_env.add_extension(MarkdownExtension) \n+ \n+ self.router.add_get(\"/{tail:.*}\",self.handle_document)\n+\n+ async def handle_document(self, request):\n+ relative_path = request.match_info['tail'].strip(\"/\")\n+ if relative_path == '':\n+ relative_path = 'index.html'\n+ document_path = self.path.joinpath(relative_path)\n+ if not document_path.exists():\n+ return web.Response(status=404,body=b'Resource is not found on this server.',content_type=\"text/plain\")\n+ if document_path.is_dir():\n+ document_path = document_path.joinpath(\"index.html\")\n+ if not document_path.exists():\n+ return web.Response(status=404,body=b'Resource is not found on this server.',content_type=\"text/plain\")\n+ \n+ response = await self.render_template(str(document_path.relative_to(self.path)),request)\n+ return response\n+\ndiff --git a/src/snek/docs/docs/api.html b/src/snek/docs/docs/api.html\nnew file mode 100644\nindex 0000000..e30a99d\n--- /dev/null\n+++ b/src/snek/docs/docs/api.html\n@@ -0,0 +1,61 @@\n+{% extends \"docs/base.html\" %}\n+\n+{% block main %}\n+{% markdown %}\n+\n+\n+Currently only some details about the internal API are available.\n+\n+```python\n+\n+new_user_object = await app.service.user.register(\n+ username=\"retoor\", \n+ password=\"retoorded\"\n+)\n+```\n+\n+```python\n+from snek.system import security\n+\n+var1 = security.encrypt(\"data\")\n+var2 = security.encrypt(b\"data\")\n+\n+assert(var1 == var2)\n+```\n+\n+```python\n+from snek.system.view import BaseView \n+\n+class IndexView(BaseView):\n+ \n+ async def get(self):\n+ return await self.render(\"index.html\")\n+```\n+```python\n+from snek.system.view import BaseFormView\n+from snek.form.register import RegisterForm\n+\n+class RegisterFormView(BaseFormView):\n+ \n+ form = RegisterForm\n+```\n+```python\n+app.routes.add_view(\"/your-page.html\", YourViewClass)\n+```\n+\n+{% endmarkdown %}\n+{% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/docs/docs/base.html b/src/snek/docs/docs/base.html\nnew file mode 100644\nindex 0000000..7c53bec\n--- /dev/null\n+++ b/src/snek/docs/docs/base.html\n@@ -0,0 +1,116 @@\n+\n+\n+\n+ \n+ \n+\n+\n+\n+
\n+
\n+ Snek\n+ Docs\n+
\n+
\n+ {% block main %}\n+ {% endblock %}\n+
\n+
\n+
\n+ {% markdown %}\n+ {% endmarkdown %}\n+
\n+\n+\n+\n\\ No newline at end of file\ndiff --git a/src/snek/docs/docs/form_api_javascript.html b/src/snek/docs/docs/form_api_javascript.html\nnew file mode 100644\nindex 0000000..c83ee7f\n--- /dev/null\n+++ b/src/snek/docs/docs/form_api_javascript.html\n@@ -0,0 +1,17 @@\n+{% extends \"docs/base.html\" %}\n+\n+{% block main %}\n+{% markdown %}\n+\n+ - generic-form.js \n+\n+It's just a HTML component that can be declared using an one liner. Buttons and title are specified server side.\n+```html \n+\n+```\n+\n+{% endmarkdown %}\n+{% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/docs/docs/form_api_python.html b/src/snek/docs/docs/form_api_python.html\nnew file mode 100644\nindex 0000000..d7e67bc\n--- /dev/null\n+++ b/src/snek/docs/docs/form_api_python.html\n@@ -0,0 +1,92 @@\n+{% extends \"docs/base.html\" %}\n+\n+{% block main %}\n+{% markdown %}\n+\n+ - `snek.system.form.Form`\n+ - `snek.system.form.HTMLElement`\n+ - `snek.system.form.FormInputElement`\n+ - `snek.system.form.FormButtonElement`\n+\n+Here is an example with custom validation. \n+This example contains a field that checks if user already exists. \n+If invalid, it adds an error message which automatically invalidates the field. \n+Handling of the error messages will automatically done client side.\n+\n+Forms are usaly located in `snek/form/[form name].py`.\n+\n+```python\n+from snek.system.form import Form, HTMLElement,FormInputElement,FormButtonElement\n+\n+class UsernameField(FormInputElement):\n+\n+ @property\n+ async def errors(self):\n+ result = await super().errors\n+ if self.value and await self.app.services.user.count(username=self.value):\n+ result.append(\"Username is not available.\")\n+ return result\n+\n+class RegisterForm(Form):\n+\n+ title = HTMLElement(tag=\"h1\", text=\"Register\")\n+\n+ username = UsernameField(\n+ name=\"username\", \n+ required=True,\n+ min_length=2,\n+ max_length=20,\n+ regex=r\"^[a-zA-Z0-9_]+$\",\n+ place_holder=\"Username\",\n+ type=\"text\"\n+ )\n+ email = FormInputElement(\n+ name=\"email\",\n+ required=False,\n+ regex=r\"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$\",\n+ place_holder=\"Email address\",\n+ type=\"email\"\n+ )\n+ password = FormInputElement(\n+ name=\"password\",\n+ required=True,\n+ regex=r\"^[a-zA-Z0-9_.+-]{6,}\",\n+ type=\"password\",\n+ place_holder=\"Password\"\n+ )\n+ action = FormButtonElement(\n+ name=\"action\",\n+ value=\"submit\",\n+ text=\"Register\",\n+ type=\"button\"\n+ )\n+```\n+\n+\n+```python \n+data = dict(\n+ username=dict(value=\"retoor\"),\n+ password=dict(value=\"retoorded\")\n+)\n+form.set_user_data(data)\n+\n+is_valid = await form.is_valid\n+\n+key_value_values = await form.record \n+```\n+\n+{% endmarkdown %}\n+{% endblock %}\n\\ No newline at end of file\ndiff --git a/src/snek/docs/docs/index.html b/src/snek/docs/docs/index.html\nnew file mode 100644\nindex 0000000..8ced8ab\n--- /dev/null\n+++ b/src/snek/docs/docs/index.html\n@@ -0,0 +1,37 @@\n+{% extends \"docs/base.html\" %}\n+\n+{% block main %}\n+{% markdown %}\n+\n+\n+Snek is a high customizable chat application. \n+It is made because Rocket Chat didn't fit my needs anymore. It became bloathed and very heavy commercialized. You would get upsell messages on your locally hosted instance!\n+\n+This documentation is under construction. Only the form API and the small introduction is a bit documented.\n+\n+[Small introduction / cheatsheet](/docs/docs/api.html)\n+\n+With the view classes of Snek you can render HTML and Markdown \n+\n+Snek's database model is based on Python dataset library. \n+Snek uses a model/mapper architecture build on top of that library.\n+\n+Snek does have his own components for creating and rendering forms. \n+All forms are made server side and client side is generated client side using a HTML component.\n+It's client side only one line to include a form that can validate and submit.\n+Validation is server side using REST. Page won't refresh.\n+[API Python](/docs/docs/form_api_python.html) \n+[API Javascript](/docs/docs/form_api_javascript.html)\n+\n+\n+{% endmarkdown %}\n+{% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-01-24", "line": "feat: Improve document handling and error responses", "commit": "dae877113c76b6f0eded7b2d63ef921123a2b559", "diff": "commit dae877113c76b6f0eded7b2d63ef921123a2b559\nAuthor: retoor \nDate: Fri Jan 24 23:42:24 2025 +0100\n\n Format.\n\ndiff --git a/src/snek/docs/app.py b/src/snek/docs/app.py\nindex 5e00d98..50a4245 100644\n--- a/src/snek/docs/app.py\n+++ b/src/snek/docs/app.py\n@@ -1,33 +1,43 @@\n-from app.app import Application as BaseApplication\n-import pathlib \n+import pathlib\n+\n from aiohttp import web\n+from app.app import Application as BaseApplication\n+\n from snek.system.markdown import MarkdownExtension\n \n-from snek.system.markdown import render_markdown \n \n class Application(BaseApplication):\n \n- def __init__(self, path=None, *args,**kwargs):\n+ def __init__(self, path=None, *args, **kwargs):\n self.path = pathlib.Path(path)\n template_path = self.path\n \n- super().__init__(template_path=template_path ,*args, **kwargs)\n- self.jinja2_env.add_extension(MarkdownExtension) \n- \n- self.router.add_get(\"/{tail:.*}\",self.handle_document)\n+ super().__init__(template_path=template_path, *args, **kwargs)\n+ self.jinja2_env.add_extension(MarkdownExtension)\n+\n+ self.router.add_get(\"/{tail:.*}\", self.handle_document)\n \n async def handle_document(self, request):\n- relative_path = request.match_info['tail'].strip(\"/\")\n- if relative_path == '':\n- relative_path = 'index.html'\n+ relative_path = request.match_info[\"tail\"].strip(\"/\")\n+ if relative_path == \"\":\n+ relative_path = \"index.html\"\n document_path = self.path.joinpath(relative_path)\n if not document_path.exists():\n- return web.Response(status=404,body=b'Resource is not found on this server.',content_type=\"text/plain\")\n+ return web.Response(\n+ status=404,\n+ body=b\"Resource is not found on this server.\",\n+ content_type=\"text/plain\",\n+ )\n if document_path.is_dir():\n document_path = document_path.joinpath(\"index.html\")\n if not document_path.exists():\n- return web.Response(status=404,body=b'Resource is not found on this server.',content_type=\"text/plain\")\n- \n- response = await self.render_template(str(document_path.relative_to(self.path)),request)\n- return response\n+ return web.Response(\n+ status=404,\n+ body=b\"Resource is not found on this server.\",\n+ content_type=\"text/plain\",\n+ )\n \n+ response = await self.render_template(\n+ str(document_path.relative_to(self.path)), request\n+ )\n+ return response"} +{"repo": ".", "date": "2025-01-25", "line": "feat: Added session support and login functionality", "commit": "5c69e14d7cfae8da4efab776165cc8e466edcc41", "diff": "commit 5c69e14d7cfae8da4efab776165cc8e466edcc41\nAuthor: retoor \nDate: Sat Jan 25 03:46:33 2025 +0100\n\n Added session support.\n\ndiff --git a/cache/crc321300331366.cache b/cache/crc321300331366.cache\ndeleted file mode 100644\nindex e69de29..0000000\ndiff --git a/cache/crc322507170282.cache b/cache/crc322507170282.cache\ndeleted file mode 100644\nindex e69de29..0000000\ndiff --git a/pyproject.toml b/pyproject.toml\nindex cc36846..71e90a7 100644\n--- a/pyproject.toml\n+++ b/pyproject.toml\n@@ -21,7 +21,8 @@ dependencies = [\n \"gunicorn\",\n \"imgkit\",\n \"wkhtmltopdf\",\n- \"jinja-markdown2\",\n- \"mistune\"\n+ \"mistune\",\n+ \"aiohttp-session\",\n+ \"cryptography\"\n ]\n \ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex ab19f42..0abf24b 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -17,8 +17,23 @@ from snek.view.login import LoginView\n from snek.view.login_form import LoginFormView\n from snek.view.register import RegisterView\n from snek.view.register_form import RegisterFormView\n+from snek.view.status import StatusView\n from snek.view.web import WebView\n \n+from aiohttp import web\n+from aiohttp_session import setup as session_setup, get_session as session_get, session_middleware\n+from aiohttp_session.cookie_storage import EncryptedCookieStorage\n+import base64\n+\n+SESSION_KEY = b'c79a0c5fda4b424189c427d28c9f7c34'\n+\n+@web.middleware\n+async def session_middleware(request, handler):\n+ setattr(request,\"session\", await session_get(request))\n+ response = await handler(request)\n+ return response\n+\n \n class Application(BaseApplication):\n \n@@ -31,10 +46,13 @@ class Application(BaseApplication):\n super().__init__(\n middlewares=middlewares, template_path=self.template_path, *args, **kwargs\n )\n+ session_setup(self,EncryptedCookieStorage(SESSION_KEY))\n+ self._middlewares.append(session_middleware)\n self.jinja2_env.add_extension(MarkdownExtension)\n self.setup_router()\n self.setup_services()\n \n+ \n def setup_services(self):\n self.services = SimpleNamespace(**get_services(app=self))\n self.mappers = SimpleNamespace(**get_mappers(app=self))\n@@ -51,7 +69,7 @@ class Application(BaseApplication):\n self.router.add_view(\"/about.md\", AboutMDView)\n self.router.add_view(\"/docs.html\", DocsHTMLView)\n self.router.add_view(\"/docs.md\", DocsMDView)\n-\n+ self.router.add_view(\"/status.json\",StatusView)\n self.router.add_view(\"/web.html\", WebView)\n self.router.add_view(\"/login.html\", LoginView)\n self.router.add_view(\"/login.json\", LoginFormView)\ndiff --git a/src/snek/docs/__pycache__/app.cpython-312.pyc b/src/snek/docs/__pycache__/app.cpython-312.pyc\nindex 087bb64..6f355a6 100644\nBinary files a/src/snek/docs/__pycache__/app.cpython-312.pyc and b/src/snek/docs/__pycache__/app.cpython-312.pyc differ\ndiff --git a/src/snek/static/app.js b/src/snek/static/app.js\nindex f06ee24..d8d3a8f 100644\n--- a/src/snek/static/app.js\n+++ b/src/snek/static/app.js\n@@ -67,7 +67,7 @@ class Room {\n class InlineAppElement extends HTMLElement {\n \n constructor(){\n- this.\n }\n \n }\n@@ -77,6 +77,45 @@ class Page {\n \n }\n \n+class RESTClient {\n+ debug = true \n+ \n+ async get(url, params){\n+ params = params ? params : {} \n+ const encodedParams = new URLSearchParams(params);\n+ if(encodedParams)\n+ url += '?' + encodedParams\n+ const response = await fetch(url,{\n+ method: 'GET',\n+ headers: {\n+ 'Content-Type': 'application/json'\n+ }\n+ });\n+ const result = await response.json()\n+ if(this.debug){\n+ console.debug({url:url,params:params,result:result})\n+ }\n+ return result \n+ }\n+ async post(url, data) {\n+ const response = await fetch(url,{\n+ method: 'POST',\n+ headers: {\n+ 'Content-Type': 'application/json'\n+ },\n+ body: JSON.stringify(data)\n+ });\n+\n+ const result = await response.json()\n+ if(this.debug){\n+ console.debug({url:url,params:params,result:result})\n+ }\n+ return result\n+ }\n+}\n+\n+const rest = new RESTClient()\n+\n class App {\n rooms = []\n constructor() {\n@@ -84,7 +123,9 @@ class App {\n \n \n }\n+ async post(url, data){\n \n+ }\n \n \n }\n\\ No newline at end of file\ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nindex a47c6db..e775bf4 100644\n--- a/src/snek/static/generic-form.js\n+++ b/src/snek/static/generic-form.js\n@@ -282,8 +282,10 @@ class GenericForm extends HTMLElement {\n {\n const isValid = await me.validate()\n if(isValid){\n- const isProcessed = await me.submit()\n- console.info({processed:isProcessed})\n+ const saveResult = await me.submit()\n+ if(saveResult.redirect_url){\n+ window.location.pathname = saveResult.redirect_url\n+ }\n }\n }\n }\n@@ -315,7 +317,6 @@ class GenericForm extends HTMLElement {\n if(!field.is_valid){\n me.fields[field.name].setInvalid()\n me.fields[field.name].setErrors(field.errors)\n- console.info(field.name,\"is invalid\")\n }else{\n me.fields[field.name].setValid()\n }\n@@ -323,10 +324,8 @@ class GenericForm extends HTMLElement {\n me.fields[field.name].updateAttributes()\n })\n Object.values(form.fields).forEach(field=>{\n- console.info(field.errors)\n me.fields[field.name].setErrors(field.errors)\n })\n- console.info({XX:form})\n return form['is_valid']\n }\n async submit(){\ndiff --git a/src/snek/system/model.py b/src/snek/system/model.py\nindex b41f4ba..98729b2 100644\n--- a/src/snek/system/model.py\n+++ b/src/snek/system/model.py\n@@ -212,6 +212,14 @@ class DeletedField(ModelField):\n \n class UUIDField(ModelField):\n \n+ @property\n+ def value(self):\n+ return str(self._value)\n+ \n+ @value.setter\n+ def value(self,val):\n+ self._value = str(val)\n+\n @property\n def initial_value(self):\n return str(uuid.uuid4())\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nindex 1cf5329..8eebcb0 100644\n--- a/src/snek/system/view.py\n+++ b/src/snek/system/view.py\n@@ -5,6 +5,13 @@ from snek.system.markdown import render_markdown\n \n class BaseView(web.View):\n \n+ login_required = False\n+\n+ async def _iter(self):\n+ if self.login_required and not self.session.get(\"logged_in\"):\n+ return web.HTTPFound(\"/\")\n+ return await super()._iter()\n+\n @property\n def app(self):\n return self.request.app\n@@ -16,6 +23,10 @@ class BaseView(web.View):\n async def json_response(self, data):\n return web.json_response(data)\n \n+ @property \n+ def session(self):\n+ return self.request.session\n+\n async def render_template(self, template_name, context=None):\n if template_name.endswith(\".md\"):\n response = await self.request.app.render_template(\n@@ -46,7 +57,8 @@ class BaseFormView(BaseView):\n pass\n if post.get(\"action\") == \"submit\" and result[\"is_valid\"]:\n- await self.submit(form)\n+ result = await self.submit(form)\n+ return await self.json_response(result)\n return await self.json_response(result)\n \n async def submit(self, model=None):\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex e00c886..cb8fc5d 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -4,6 +4,7 @@\n \n \n {% block title %}{% endblock %}\n+ \n \n \n \ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex 0403e1b..5a636f0 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -3,12 +3,13 @@\n \n \n \n- Dark Themed Chat Application\n+ Snek\n+ \n \n \n \n
\n-
Molodetz Chat
\n+
Snek
\n
\n
\n {% block sidebar %}\n- \n+ {% include \"sidebar_channels.html\" %}\n {% endblock %}\n {% block main %}\n \ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex fb935c8..bdcddae 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -132,6 +132,7 @@\n app.addEventListener(\"channel-message\", (data) => {\n if (data.channel_uid !== channelUid) {\n if(!isMentionForSomeoneElse(data.message)){\n+ channelSidebar.notify(data);\n app.playSound(\"messageOtherChannel\");\n }\n \ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex dda589f..5a3b264 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -52,7 +52,7 @@ class WebView(BaseView):\n other_user = await self.app.services.channel_member.get_other_dm_user(subscribed_channel[\"channel_uid\"], self.session.get(\"uid\"))\n if other_user:\n item[\"name\"] = other_user[\"nick\"]\n- item[\"uid\"] = other_user[\"uid\"]\n+ item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n else:\n item[\"name\"] = subscribed_channel[\"label\"]\n item[\"uid\"] = subscribed_channel[\"channel_uid\"]"} +{"repo": ".", "date": "2025-02-22", "line": "feat: Added channel sidebar with message counts and highlighting", "commit": "076fbb30fb51ecfb5b15d394c760f55dac26e1c1", "diff": "commit 076fbb30fb51ecfb5b15d394c760f55dac26e1c1\nAuthor: retoor \nDate: Sat Feb 22 01:21:57 2025 +0100\n\n Added sidebar stuff.\n\ndiff --git a/src/snek/templates/sidebar_channels.html b/src/snek/templates/sidebar_channels.html\nnew file mode 100644\nindex 0000000..ed16ffd\n--- /dev/null\n+++ b/src/snek/templates/sidebar_channels.html\n@@ -0,0 +1,64 @@\n+\n+\n+ \n\\ No newline at end of file"} +{"repo": ".", "date": "2025-02-26", "line": "feat: Added avatar support and display in message view.", "commit": "5af4e5754b6902ae13c798d9793281d62b684590", "diff": "commit 5af4e5754b6902ae13c798d9793281d62b684590\nAuthor: retoor \nDate: Wed Feb 26 00:22:36 2025 +0100\n\n Update avatar.\n\ndiff --git a/Makefile b/Makefile\nindex f153fc1..e3febf7 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -5,13 +5,15 @@ GUNICORN=./.venv/bin/gunicorn\n GUNICORN_WORKERS = 1\n PORT = 8081\n \n+python:\n+\t$(PYTHON)\n \n+run:\n+\t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\n+\t\n install:\n \tpython3 -m venv .venv \n \t$(PIP) install -e .\n \n \n \n-run:\n-\t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\n-\t\ndiff --git a/pyproject.toml b/pyproject.toml\nindex 2d5b6d9..e1075c9 100644\n--- a/pyproject.toml\n+++ b/pyproject.toml\n@@ -28,6 +28,7 @@ dependencies = [\n \"asyncssh\",\n \"emoji\",\n \"aiofiles\",\n- \"PyJWT\"\n+ \"PyJWT\",\n+ \"multiavatar\"\n ]\n \ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 3a61c2b..99f23e7 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -33,8 +33,10 @@ from snek.view.status import StatusView\n from snek.view.web import WebView\n from snek.view.upload import UploadView\n from snek.view.search_user import SearchUserView \n+from snek.view.avatar import AvatarView\n from snek.system.profiler import profiler_handler\n \n+\n SESSION_KEY = b\"c79a0c5fda4b424189c427d28c9f7c34\"\n \n \n@@ -101,6 +103,7 @@ class Application(BaseApplication):\n self.router.add_view(\"/drive.bin/{uid}.{ext}\", UploadView)\n self.router.add_view(\"/search-user.html\", SearchUserView)\n self.router.add_view(\"/search-user.json\", SearchUserView)\n+ self.router.add_view(\"/avatar/{uid}.svg\", AvatarView)\n self.router.add_get(\"/http-get\", self.handle_http_get)\n self.router.add_get(\"/http-photo\", self.handle_http_photo)\n self.router.add_get(\"/rpc.ws\", RPCView)\ndiff --git a/src/snek/templates/message.html b/src/snek/templates/message.html\nindex 2773f0a..e8afc31 100644\n--- a/src/snek/templates/message.html\n+++ b/src/snek/templates/message.html\n@@ -1 +1 @@\n-
{{user_nick[0]}}
{{user_nick}}
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}
\n+
{{user_nick}}
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}
\ndiff --git a/src/snek/view/avatar.py b/src/snek/view/avatar.py\nnew file mode 100644\nindex 0000000..065ff6a\n--- /dev/null\n+++ b/src/snek/view/avatar.py\n@@ -0,0 +1,39 @@\n+\n+\n+\n+from multiavatar import multiavatar\n+\n+from aiohttp import web\n+from snek.system.view import BaseView\n+\n+class AvatarView(BaseView):\n+ login_required = True\n+\n+ async def get(self):\n+ uid = self.request.match_info.get(\"uid\")\n+ avatar = multiavatar.multiavatar(uid,None, None)\n+ response = web.Response(text=avatar, content_type='image/svg+xml')\n+ response.headers['Cache-Control'] = f'public, max-age={1337*42}'\n+ return response\ndiff --git a/src/snek/view/upload.py b/src/snek/view/upload.py\nindex 05ffcaa..a80ec72 100644\n--- a/src/snek/view/upload.py\n+++ b/src/snek/view/upload.py\n@@ -23,7 +23,9 @@ class UploadView(BaseView):\n async def get(self):\n uid = self.request.match_info.get(\"uid\")\n drive_item = await self.services.drive_item.get(uid)\n- return web.FileResponse(drive_item[\"path\"])\n+ response = web.FileResponse(drive_item[\"path\"])\n+ response.headers['Cache-Control'] = f'public, max-age={1337*420}'\n+ return response\n \n async def post(self):\n reader = await self.request.multipart()"} +{"repo": ".", "date": "2025-02-26", "line": "feat: Generate unique avatar IDs when requested", "commit": "e280e8776457605bbb5548fe9de5328b7b04bb8a", "diff": "commit e280e8776457605bbb5548fe9de5328b7b04bb8a\nAuthor: retoor \nDate: Wed Feb 26 00:40:18 2025 +0100\n\n Update avatar.\n\ndiff --git a/src/snek/view/avatar.py b/src/snek/view/avatar.py\nindex 065ff6a..8281df1 100644\n--- a/src/snek/view/avatar.py\n+++ b/src/snek/view/avatar.py\n@@ -24,7 +24,7 @@\n from multiavatar import multiavatar\n-\n+import uuid\n from aiohttp import web\n from snek.system.view import BaseView\n \n@@ -33,6 +33,8 @@ class AvatarView(BaseView):\n \n async def get(self):\n uid = self.request.match_info.get(\"uid\")\n+ if uid == \"unique\":\n+ uid = str(uuid.uuid4())\n avatar = multiavatar.multiavatar(uid,None, None)\n response = web.Response(text=avatar, content_type='image/svg+xml')\n response.headers['Cache-Control'] = f'public, max-age={1337*42}'"} +{"repo": ".", "date": "2025-02-26", "line": "feat: Generate unique avatars", "commit": "162cfe394558a085894a11cea57b193d6108b90e", "diff": "commit 162cfe394558a085894a11cea57b193d6108b90e\nAuthor: retoor \nDate: Wed Feb 26 00:46:48 2025 +0100\n\n Update avatar.\n\ndiff --git a/src/snek/view/avatar.py b/src/snek/view/avatar.py\nindex 8281df1..54dbec0 100644\n--- a/src/snek/view/avatar.py\n+++ b/src/snek/view/avatar.py\n@@ -35,7 +35,7 @@ class AvatarView(BaseView):\n uid = self.request.match_info.get(\"uid\")\n if uid == \"unique\":\n uid = str(uuid.uuid4())\n- avatar = multiavatar.multiavatar(uid,None, None)\n+ avatar = multiavatar.multiavatar(uid, True, None)\n response = web.Response(text=avatar, content_type='image/svg+xml')\n response.headers['Cache-Control'] = f'public, max-age={1337*42}'\n return response"} +{"repo": ".", "date": "2025-02-26", "line": "feat: Updated welcome message and registration buttons", "commit": "da1be6301c79cf76383e6568f91ee23bdf5119f6", "diff": "commit da1be6301c79cf76383e6568f91ee23bdf5119f6\nAuthor: retoor \nDate: Wed Feb 26 06:21:48 2025 +0100\n\n Text\n\ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nindex c6d57df..1894bc4 100644\n--- a/src/snek/templates/index.html\n+++ b/src/snek/templates/index.html\n@@ -11,12 +11,11 @@\n \n
\n

Snek

\n+

Rocket Chat got bloated, too commercialized,\n+ So Snek came through, lean and optimized.

\n \n- Or\n+ OR\n \n- Design choices\n- App preview\n- API docs\n
\n \n "} +{"repo": ".", "date": "2025-02-28", "line": "fix: Disable login requirement for avatar view", "commit": "66b85d146abac25df83edc1975db209b9d43fae7", "diff": "commit 66b85d146abac25df83edc1975db209b9d43fae7\nAuthor: retoor \nDate: Fri Feb 28 15:42:52 2025 +0100\n\n Non required avatar stuff.\n\ndiff --git a/src/snek/view/avatar.py b/src/snek/view/avatar.py\nindex 54dbec0..cbd973c 100644\n--- a/src/snek/view/avatar.py\n+++ b/src/snek/view/avatar.py\n@@ -29,7 +29,7 @@ from aiohttp import web\n from snek.system.view import BaseView\n \n class AvatarView(BaseView):\n- login_required = True\n+ login_required = False\n \n async def get(self):\n uid = self.request.match_info.get(\"uid\")"} +{"repo": ".", "date": "2025-03-02", "line": "feat: Add last_message_on to ChannelModel and update on message send", "commit": "4620ebb800b5dd848ec28713f1afa20416698922", "diff": "commit 4620ebb800b5dd848ec28713f1afa20416698922\nAuthor: retoor \nDate: Sun Mar 2 14:49:38 2025 +0100\n\n Channel check.\n\ndiff --git a/src/snek/model/channel.py b/src/snek/model/channel.py\nindex d664087..8a40ced 100644\n--- a/src/snek/model/channel.py\n+++ b/src/snek/model/channel.py\n@@ -9,3 +9,4 @@ class ChannelModel(BaseModel):\n is_private = ModelField(name=\"is_private\", required=True, kind=bool, value=False)\n is_listed = ModelField(name=\"is_listed\", required=True, kind=bool, value=True)\n index = ModelField(name=\"index\", required=True, kind=int, value=1000)\n+ last_message_on = ModelField(name=\"last_message_on\", required=False, kind=str)\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex 02a4bd5..5eacbeb 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -1,12 +1,16 @@\n \n \n \n+from snek.system.model import now\n from snek.system.service import BaseService\n \n \n class ChatService(BaseService):\n \n async def send(self,user_uid, channel_uid, message):\n+ channel = await self.services.channel.get(uid=channel_uid)\n+ if not channel:\n+ raise Exception(\"Channel not found.\")\n channel_message = await self.services.channel_message.create(\n channel_uid, \n user_uid, \n@@ -28,4 +32,6 @@ class ChatService(BaseService):\n uid=channel_message['uid'],\n user_nick=user['nick']\n ))\n+ channel['last_message_on'] = now()\n+ await self.services.channel.save(channel)\n return sent_to_count\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-03-02", "line": "feat: Add index creation with error handling", "commit": "e469e27abfedc0b08b483e0715b4dc9b16240c5e", "diff": "commit e469e27abfedc0b08b483e0715b4dc9b16240c5e\nAuthor: retoor \nDate: Sun Mar 2 14:59:25 2025 +0100\n\n Optinal indexes.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 99f23e7..6d9346d 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -67,13 +67,17 @@ class Application(BaseApplication):\n self.setup_router()\n self.db.query(\"PRAGMA journal_mode=WAL\")\n self.db.query(\"PRAGMA syncnorm=off\")\n- if not self.db[\"user\"].has_index(\"username\"):\n- self.db[\"user\"].create_index(\"username\", unique=True)\n- if not self.db[\"channel_member\"].has_index([\"channel_uid\",\"user_uid\"]):\n- self.db[\"channel_member\"].create_index([\"channel_uid\",\"user_uid\"])\n- if not self.db[\"channel_message\"].has_index([\"channel_uid\",\"user_uid\"]):\n- self.db[\"channel_message\"].create_index([\"channel_uid\",\"user_uid\"])\n- \n+\n+ try:\n+ if not self.db[\"user\"].has_index(\"username\"):\n+ self.db[\"user\"].create_index(\"username\", unique=True)\n+ if not self.db[\"channel_member\"].has_index([\"channel_uid\",\"user_uid\"]):\n+ self.db[\"channel_member\"].create_index([\"channel_uid\",\"user_uid\"])\n+ if not self.db[\"channel_message\"].has_index([\"channel_uid\",\"user_uid\"]):\n+ self.db[\"channel_message\"].create_index([\"channel_uid\",\"user_uid\"])\n+ except:\n+ pass \n+ \n self.cache = Cache(self)\n self.services = get_services(app=self)\n self.mappers = get_mappers(app=self)"} +{"repo": ".", "date": "2025-03-03", "line": "fix: Correctly handle trailing commas in link targets and improve upload view display", "commit": "45e3239cc06cdab0a8e5c1c1ef56593f65e750ea", "diff": "commit 45e3239cc06cdab0a8e5c1c1ef56593f65e750ea\nAuthor: retoor \nDate: Mon Mar 3 00:35:08 2025 +0100\n\n Upload changes.\n\ndiff --git a/src/snek/system/template.py b/src/snek/system/template.py\nindex 22d007d..0e71b80 100644\n--- a/src/snek/system/template.py\n+++ b/src/snek/system/template.py\n@@ -73,7 +73,7 @@ def set_link_target_blank(text):\n element.attrs['target'] = '_blank'\n element.attrs['rel'] = 'noopener noreferrer'\n element.attrs['referrerpolicy'] = 'no-referrer'\n- element.attrs['href'] = element.attrs['href'].strip(\".\")\n+ element.attrs['href'] = element.attrs['href'].strip(\".\").strip(\",\")\n \n return str(soup)\n \ndiff --git a/src/snek/view/upload.py b/src/snek/view/upload.py\nindex a80ec72..f9bad33 100644\n--- a/src/snek/view/upload.py\n+++ b/src/snek/view/upload.py\n@@ -77,7 +77,7 @@ class UploadView(BaseView):\n await self.services.drive_item.save(drive_item)\n response = \"Uploaded [\" + filename + \"](/drive.bin/\" + drive_item[\"uid\"] + \")\"\n- response = \"[url](/drive.bin/\" + drive_item[\"uid\"] + extension + \")\"\n+ response = \"[\" + filename + \"](/drive.bin/\" + drive_item[\"uid\"] + extension + \")\"\n \n await self.services.chat.send(\n self.request.session.get(\"uid\"), channel_uid, response"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Implement push notifications for service worker", "commit": "c3c94461c295ef4c6219051369472f983267437c", "diff": "commit c3c94461c295ef4c6219051369472f983267437c\nAuthor: retoor \nDate: Wed Mar 5 17:19:24 2025 +0100\n\n Update service worker.\n\ndiff --git a/src/snek/static/service-worker.js b/src/snek/static/service-worker.js\nindex dfdc493..5881455 100644\n--- a/src/snek/static/service-worker.js\n+++ b/src/snek/static/service-worker.js\n@@ -1,3 +1,37 @@\n+async function requestNotificationPermission() {\n+ const permission = await Notification.requestPermission();\n+ return permission === 'granted';\n+}\n+\n+async function subscribeUser() {\n+ const registration = await navigator.serviceWorker.register('/service-worker.js');\n+ \n+ const subscription = await registration.pushManager.subscribe({\n+ userVisibleOnly: true,\n+ applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)\n+ });\n+\n+ await fetch('/subscribe', {\n+ method: 'POST',\n+ body: JSON.stringify(subscription),\n+ headers: {\n+ 'Content-Type': 'application/json'\n+ }\n+ });\n+}\n+\n+self.addEventListener('push', event => {\n+ const data = event.data.json();\n+ self.registration.showNotification(data.title, {\n+ body: data.message,\n+ icon: data.icon\n+ });\n+});\n+\n self.addEventListener(\"install\", (event) => {\n console.log(\"Service worker installed\");\n });\n@@ -27,4 +61,4 @@ self.addEventListener(\"notificationclick\", (event) => {\n event.notification.close();\n event.waitUntil(clients.openWindow(\n-});"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Implement notification read functionality and unread stats", "commit": "578c182f2707a5f5b7c93f421e2035f7271aa60c", "diff": "commit 578c182f2707a5f5b7c93f421e2035f7271aa60c\nAuthor: retoor \nDate: Wed Mar 5 17:51:25 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 3815c60..27c65f4 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -1,9 +1,17 @@\n from snek.system.service import BaseService\n-\n+from snek.system.model import now\n \n class NotificationService(BaseService):\n mapper_name = \"notification\"\n \n+ async def mark_as_read(self, user_uid, channel_message_uid):\n+ model = await self.get(user_uid, object_uid=channel_message_uid)\n+ model['read_at'] = now()\n+ await self.save(model)\n+\n+ async def get_unread_stats(self,user_uid):\n+ 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))\n+\n async def create(self, object_uid, object_type, user_uid, message):\n model = await self.new()\n model[\"object_uid\"] = object_uid\ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex d98e53b..c197f24 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -39,6 +39,11 @@ class RPCView(BaseView):\n def is_logged_in(self):\n return self.view.session.get(\"logged_in\", False)\n \n+ async def mark_as_read(self, message_uid):\n+ self._require_login()\n+ await self.services.notification.mark_as_read(self.user_uid, message_uid)\n+ return True\n+\n async def login(self, username, password):\n success = await self.services.user.validate_login(username, password)\n if not success:\ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex 5a3b264..8fd5ddc 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -46,6 +46,9 @@ class WebView(BaseView):\n messages = [await self.app.services.channel_message.to_extended_dict(message) for message in await self.app.services.channel_message.offset(\n channel[\"uid\"]\n )]\n+ for message in messages:\n+ await self.app.services.notification.mark_as_read(self.session.get(\"uid\"),message[\"uid\"])\n+\n channels = []\n async for subscribed_channel in self.app.services.channel_member.find(user_uid=self.session.get(\"uid\"), deleted_at=None, is_banned=False):\n item = {}"} +{"repo": ".", "date": "2025-03-05", "line": "fix: Handle missing notification model in mark_as_read", "commit": "580ec5ab0d57a542ab38b25a6c508804d5bcfa21", "diff": "commit 580ec5ab0d57a542ab38b25a6c508804d5bcfa21\nAuthor: retoor \nDate: Wed Mar 5 17:52:18 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 27c65f4..6db762c 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -6,9 +6,12 @@ class NotificationService(BaseService):\n \n async def mark_as_read(self, user_uid, channel_message_uid):\n model = await self.get(user_uid, object_uid=channel_message_uid)\n+ if not model:\n+ return False \n model['read_at'] = now()\n await self.save(model)\n-\n+ return True \n+ \n async def get_unread_stats(self,user_uid):\n 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))"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Add new_count to ChannelMember and increment on notification", "commit": "84d7b11f24b37cdc41ce9d5bb24be4080af14be9", "diff": "commit 84d7b11f24b37cdc41ce9d5bb24be4080af14be9\nAuthor: retoor \nDate: Wed Mar 5 17:59:59 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/model/channel_member.py b/src/snek/model/channel_member.py\nindex d199498..65ba3e4 100644\n--- a/src/snek/model/channel_member.py\n+++ b/src/snek/model/channel_member.py\n@@ -13,3 +13,4 @@ class ChannelMemberModel(BaseModel):\n )\n is_muted = ModelField(name=\"is_muted\", required=True, kind=bool, value=False)\n is_banned = ModelField(name=\"is_banned\", required=True, kind=bool, value=False)\n+ new_count = ModelField(name=\"new_count\", required=False, kind=int, value=0)\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 6db762c..ad0acbe 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -36,6 +36,8 @@ class NotificationService(BaseService):\n is_muted=False,\n deleted_at=None,\n ):\n+ channel_member['new_count'] += 1\n+ await self.services.channel_member.save(channel_member)\n model = await self.new()\n model[\"object_uid\"] = channel_message_uid\n model[\"object_type\"] = \"channel_message\""} +{"repo": ".", "date": "2025-03-05", "line": "feat: Display new notification count in RPC view", "commit": "d7851e645785fd707a7c7fdc5b6fff036e0c80f7", "diff": "commit d7851e645785fd707a7c7fdc5b6fff036e0c80f7\nAuthor: retoor \nDate: Wed Mar 5 18:02:02 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex c197f24..55accc7 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -94,6 +94,7 @@ class RPCView(BaseView):\n \"name\": subscription[\"label\"],\n \"uid\": subscription[\"channel_uid\"],\n \"tag\": channel[\"tag\"],\n+ \"new_count\": subscription[\"new_count\"],\n \"is_moderator\": subscription[\"is_moderator\"],\n \"is_read_only\": subscription[\"is_read_only\"]\n })"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Increment new_count for channel messages and members", "commit": "e1324e99bf06018a804a1a3dcc83f96cde04b1af", "diff": "commit e1324e99bf06018a804a1a3dcc83f96cde04b1af\nAuthor: retoor \nDate: Wed Mar 5 18:05:34 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex 5eacbeb..54e60dc 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -17,7 +17,10 @@ class ChatService(BaseService):\n message\n )\n channel_message_uid = channel_message[\"uid\"]\n- \n+ if not channel_message['new_count']:\n+ channel_message['new_count'] = 0\n+ channel_message['new_count'] += 1\n+ await self.services.channel_message.save(channel_message)\n user = await self.services.user.get(uid=user_uid)\n await self.services.notification.create_channel_message(channel_message_uid)\n sent_to_count = await self.services.socket.broadcast(channel_uid, dict(\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex ad0acbe..6db762c 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -36,8 +36,6 @@ class NotificationService(BaseService):\n is_muted=False,\n deleted_at=None,\n ):\n- channel_member['new_count'] += 1\n- await self.services.channel_member.save(channel_member)\n model = await self.new()\n model[\"object_uid\"] = channel_message_uid\n model[\"object_type\"] = \"channel_message\""} +{"repo": ".", "date": "2025-03-05", "line": "feat: Increment new_count in chat and notification services", "commit": "afbf53938bd59e1e03dfc011063b27134dd0c054", "diff": "commit afbf53938bd59e1e03dfc011063b27134dd0c054\nAuthor: retoor \nDate: Wed Mar 5 18:09:54 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex 54e60dc..17c677b 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -17,10 +17,8 @@ class ChatService(BaseService):\n message\n )\n channel_message_uid = channel_message[\"uid\"]\n- if not channel_message['new_count']:\n- channel_message['new_count'] = 0\n- channel_message['new_count'] += 1\n- await self.services.channel_message.save(channel_message)\n+ \n+ \n user = await self.services.user.get(uid=user_uid)\n await self.services.notification.create_channel_message(channel_message_uid)\n sent_to_count = await self.services.socket.broadcast(channel_uid, dict(\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 6db762c..a703f23 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -36,6 +36,11 @@ class NotificationService(BaseService):\n is_muted=False,\n deleted_at=None,\n ):\n+ if not channel_member['new_count']:\n+ channel_member['new_count'] = 0\n+ channel_member['new_count'] += 1\n+ await self.services.channel_member.save(channel_member)\n+ \n model = await self.new()\n model[\"object_uid\"] = channel_message_uid\n model[\"object_type\"] = \"channel_message\""} +{"repo": ".", "date": "2025-03-05", "line": "feat: Added new_count to notifications", "commit": "8d3d7327d777ae3150ecaa237e137f8221310dfa", "diff": "commit 8d3d7327d777ae3150ecaa237e137f8221310dfa\nAuthor: retoor \nDate: Wed Mar 5 18:17:43 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex a703f23..f5917e7 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -21,6 +21,7 @@ class NotificationService(BaseService):\n model[\"object_type\"] = object_type\n model[\"user_uid\"] = user_uid\n model[\"message\"] = message\n+ model['new_count'] = 1337\n if await self.save(model):\n return model\n raise Exception(f\"Failed to create notification: {model.errors}.\")\n@@ -40,7 +41,7 @@ class NotificationService(BaseService):\n channel_member['new_count'] = 0\n channel_member['new_count'] += 1\n await self.services.channel_member.save(channel_member)\n- \n+\n model = await self.new()\n model[\"object_uid\"] = channel_message_uid\n model[\"object_type\"] = \"channel_message\""} +{"repo": ".", "date": "2025-03-05", "line": "feat: Remove unused new_count field from notification model", "commit": "4df6055566d61c769ae1759d81900d138093136e", "diff": "commit 4df6055566d61c769ae1759d81900d138093136e\nAuthor: retoor \nDate: Wed Mar 5 18:18:06 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex f5917e7..399a0a1 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -21,7 +21,6 @@ class NotificationService(BaseService):\n model[\"object_type\"] = object_type\n model[\"user_uid\"] = user_uid\n model[\"message\"] = message\n- model['new_count'] = 1337\n if await self.save(model):\n return model\n raise Exception(f\"Failed to create notification: {model.errors}.\")"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Increment notification count and add debug print", "commit": "dd11c8da5acc5ed97a278a705e7282edc7d50bc5", "diff": "commit dd11c8da5acc5ed97a278a705e7282edc7d50bc5\nAuthor: retoor \nDate: Wed Mar 5 18:18:48 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 399a0a1..5a301a8 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -39,6 +39,7 @@ class NotificationService(BaseService):\n if not channel_member['new_count']:\n channel_member['new_count'] = 0\n channel_member['new_count'] += 1\n+ print(\"INSERTED!!\",flush=True)\n await self.services.channel_member.save(channel_member)\n \n model = await self.new()"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Log model records during upsert", "commit": "8f137cf8e6d67a8e73ee221d2f9192b7a3ab431d", "diff": "commit 8f137cf8e6d67a8e73ee221d2f9192b7a3ab431d\nAuthor: retoor \nDate: Wed Mar 5 18:24:32 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 5a301a8..399a0a1 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -39,7 +39,6 @@ class NotificationService(BaseService):\n if not channel_member['new_count']:\n channel_member['new_count'] = 0\n channel_member['new_count'] += 1\n- print(\"INSERTED!!\",flush=True)\n await self.services.channel_member.save(channel_member)\n \n model = await self.new()\ndiff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py\nindex d7e4163..e57b0fa 100644\n--- a/src/snek/system/mapper.py\n+++ b/src/snek/system/mapper.py\n@@ -49,6 +49,7 @@ class BaseMapper:\n if not model.record.get(\"uid\"):\n raise Exception(f\"Attempt to save without uid: {model.record}.\")\n model.updated_at.update()\n+ print(model.record,flush=True)\n return self.table.upsert(model.record, [\"uid\"])\n \n async def find(self, **kwargs) -> typing.AsyncGenerator:"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Allow channel members to be created", "commit": "30fe0bfae7f2bc9badcb16f734655e661fe976ad", "diff": "commit 30fe0bfae7f2bc9badcb16f734655e661fe976ad\nAuthor: retoor \nDate: Wed Mar 5 18:37:44 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/channel_member.py b/src/snek/service/channel_member.py\nindex f34f08b..191a063 100644\n--- a/src/snek/service/channel_member.py\n+++ b/src/snek/service/channel_member.py\n@@ -5,6 +5,7 @@ class ChannelMemberService(BaseService):\n \n mapper_name = \"channel_member\"\n \n+\n async def create(\n self,\n channel_uid,\ndiff --git a/src/snek/system/service.py b/src/snek/system/service.py\nindex d65f947..9feb759 100644\n--- a/src/snek/system/service.py\n+++ b/src/snek/system/service.py\n@@ -61,7 +61,7 @@ class BaseService:\n if not \"_limit\" in kwargs or int(kwargs.get(\"_limit\")) > 30:\n kwargs[\"_limit\"] = 60\n async for model in self.mapper.find(**kwargs):\n- yield model\n+ yield await self.get(uid=model[\"uid\"])\n \n async def delete(self, **kwargs):\n return await self.mapper.delete(**kwargs)"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Return model directly from query", "commit": "edb35b57070a5659e8491a967880d816f2d07697", "diff": "commit edb35b57070a5659e8491a967880d816f2d07697\nAuthor: retoor \nDate: Wed Mar 5 18:40:53 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/system/service.py b/src/snek/system/service.py\nindex 9feb759..d65f947 100644\n--- a/src/snek/system/service.py\n+++ b/src/snek/system/service.py\n@@ -61,7 +61,7 @@ class BaseService:\n if not \"_limit\" in kwargs or int(kwargs.get(\"_limit\")) > 30:\n kwargs[\"_limit\"] = 60\n async for model in self.mapper.find(**kwargs):\n- yield await self.get(uid=model[\"uid\"])\n+ yield model\n \n async def delete(self, **kwargs):\n return await self.mapper.delete(**kwargs)"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Display username when inserting notification", "commit": "0613f6f54de9247c17b4bae924197ffb4cbd2966", "diff": "commit 0613f6f54de9247c17b4bae924197ffb4cbd2966\nAuthor: retoor \nDate: Wed Mar 5 18:48:27 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 399a0a1..f8a87c8 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -39,6 +39,9 @@ class NotificationService(BaseService):\n if not channel_member['new_count']:\n channel_member['new_count'] = 0\n channel_member['new_count'] += 1\n+ \n+ usr = await self.services.user.get(uid=channel_member[\"user_uid\"])\n+ print(\"INSERTED FOR \",usr[\"username\"],flush=True)\n await self.services.channel_member.save(channel_member)\n \n model = await self.new()"} +{"repo": ".", "date": "2025-03-05", "line": "feat: Disable banned, muted, and deleted checks in channel member query", "commit": "1807cff67d3a4306f122d7df4436cc88137f299c", "diff": "commit 1807cff67d3a4306f122d7df4436cc88137f299c\nAuthor: retoor \nDate: Wed Mar 5 18:51:50 2025 +0100\n\n Notifications accept.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex f8a87c8..6041901 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -32,9 +32,9 @@ class NotificationService(BaseService):\n user = await self.services.user.get(uid=channel_message[\"user_uid\"])\n async for channel_member in self.services.channel_member.find(\n channel_uid=channel_message[\"channel_uid\"],\n- is_banned=False,\n- is_muted=False,\n- deleted_at=None,\n ):\n if not channel_member['new_count']:\n channel_member['new_count'] = 0"} +{"repo": ".", "date": "2025-03-07", "line": "feat: Implemented threads view with basic message display", "commit": "5b78165fb7e83f7e8a45f790c0f6e5a9fde758a0", "diff": "commit 5b78165fb7e83f7e8a45f790c0f6e5a9fde758a0\nAuthor: retoor \nDate: Fri Mar 7 20:58:53 2025 +0100\n\n New stuff.\n\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nnew file mode 100644\nindex 0000000..2d7d01b\n--- /dev/null\n+++ b/src/snek/templates/threads.html\n@@ -0,0 +1,153 @@\n+{% extends \"app.html\" %}\n+\n+{% block main %}\n+
\n+
\n+

?

\n+
\n+
\n+ {% for thread in threads %}\n+ {% autoescape false %}\n+
\n+
\n+
\n+
{{thread.last_message_user_nick}}
\n+
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ thread.last_message_text }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{%\n+ endautoescape %}
\n+
\n+
\n+
\n+\n+ {% endautoescape %}\n+ {% endfor %}\n+
\n+ \n+
\n+\n+\n+{% endblock %}\ndiff --git a/src/snek/view/threads.py b/src/snek/view/threads.py\nnew file mode 100644\nindex 0000000..d8f4359\n--- /dev/null\n+++ b/src/snek/view/threads.py\n@@ -0,0 +1,31 @@\n+from snek.system.view import BaseView\n+\n+class ThreadsView(BaseView):\n+\n+ async def get(self):\n+ threads = []\n+ user = await self.services.user.get(uid=self.session.get(\"uid\"))\n+ async for channel_member in user.get_channel_members():\n+ thread = {}\n+ channel = await self.services.channel.get(uid=channel_member[\"channel_uid\"])\n+ thread[\"uid\"] = channel['uid']\n+ thread[\"name\"] = await channel_member.get_name()\n+ thread[\"new_count\"] = channel_member[\"new_count\"]\n+ thread[\"last_message_on\"] = channel[\"last_message_on\"]\n+ thread['created_at'] = thread['last_message_on']\n+ last_message = await channel.get_last_message()\n+ if last_message:\n+ thread[\"last_message_text\"] = last_message[\"message\"]\n+ thread['last_message_user_uid'] = last_message[\"user_uid\"]\n+ user_last_message = await self.app.services.user.get(uid=last_message[\"user_uid\"])\n+ thread['last_message_user_nick'] = user_last_message[\"nick\"]\n+ thread['last_message_user_color'] = user_last_message['color']\n+ else:\n+ thread[\"last_message_text\"] = None \n+ thread['last_message_user_uid'] = None \n+ thread['last_message_user_nick'] = None \n+ thread['last_message_user_color'] = None\n+ threads.append(thread)\n+\n+\n+ return await self.render_template(\"threads.html\", dict(threads=threads,user=user))"} +{"repo": ".", "date": "2025-03-07", "line": "feat: Added threads view and related model updates", "commit": "a1afaacb2ea6ded2eb14eb6d8fbdaf34708d9568", "diff": "commit a1afaacb2ea6ded2eb14eb6d8fbdaf34708d9568\nAuthor: retoor \nDate: Fri Mar 7 20:59:11 2025 +0100\n\n New stuff.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 6d9346d..e1f74e2 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -1,7 +1,9 @@\n import pathlib\n import asyncio\n \n-import logging \n+import logging\n+\n+from snek.view.threads import ThreadsView \n \n logging.basicConfig(level=logging.DEBUG)\n \n@@ -112,6 +114,7 @@ class Application(BaseApplication):\n self.router.add_get(\"/http-photo\", self.handle_http_photo)\n self.router.add_get(\"/rpc.ws\", RPCView)\n self.router.add_view(\"/channel/{channel}.html\", WebView)\n+ self.router.add_view(\"/threads.html\", ThreadsView)\n \n self.add_subapp(\n \"/docs\",\ndiff --git a/src/snek/model/channel.py b/src/snek/model/channel.py\nindex 8a40ced..0070948 100644\n--- a/src/snek/model/channel.py\n+++ b/src/snek/model/channel.py\n@@ -1,3 +1,4 @@\n+from snek.model.channel_message import ChannelMessageModel\n from snek.system.model import BaseModel, ModelField\n \n \n@@ -10,3 +11,11 @@ class ChannelModel(BaseModel):\n is_listed = ModelField(name=\"is_listed\", required=True, kind=bool, value=True)\n index = ModelField(name=\"index\", required=True, kind=int, value=1000)\n last_message_on = ModelField(name=\"last_message_on\", required=False, kind=str)\n+\n+ async def get_last_message(self)->ChannelMessageModel:\n+ async for model in self.app.services.channel_message.query(\"SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1\",dict(channel_uid=self['uid'])):\n+ return model \n+ return None\n+\n+ async def get_members(self):\n+ return await self.app.services.channel_member.find(channel_uid=self['uid'],deleted_at=None,is_banned=False)\n\\ No newline at end of file\ndiff --git a/src/snek/model/channel_member.py b/src/snek/model/channel_member.py\nindex 65ba3e4..5a8332e 100644\n--- a/src/snek/model/channel_member.py\n+++ b/src/snek/model/channel_member.py\n@@ -14,3 +14,27 @@ class ChannelMemberModel(BaseModel):\n is_muted = ModelField(name=\"is_muted\", required=True, kind=bool, value=False)\n is_banned = ModelField(name=\"is_banned\", required=True, kind=bool, value=False)\n new_count = ModelField(name=\"new_count\", required=False, kind=int, value=0)\n+\n+ async def get_user(self):\n+ return await self.app.services.user.get(uid=self['user_uid'])\n+ \n+ async def get_channel(self):\n+ return await self.app.services.channel.get(uid=self['channel_uid'])\n+\n+ async def get_name(self):\n+ if self[\"channel_uid\"] == \"dm\":\n+ user = await self.get_other_dm_user()\n+ return user['nick']\n+ channel = await self.get_channel()\n+ return channel['name']\n+\n+ async def get_other_dm_user(self):\n+ channel = await self.get_channel()\n+ if channel[\"tag\"] != \"dm\":\n+ return None\n+ \n+ async for model in self.app.services.channel_member.find(channel_uid=channel['uid']):\n+ if model[\"uid\"] != self['uid']:\n+ return await self.app.services.user.get(uid=model[\"user_uid\"])\n+ return await self.get_user()\n+ \n\\ No newline at end of file\ndiff --git a/src/snek/model/channel_message.py b/src/snek/model/channel_message.py\nindex 0bda0bc..4b84fdc 100644\n--- a/src/snek/model/channel_message.py\n+++ b/src/snek/model/channel_message.py\n@@ -1,3 +1,4 @@\n+from snek.model.user import UserModel\n from snek.system.model import BaseModel, ModelField\n \n \n@@ -6,3 +7,9 @@ class ChannelMessageModel(BaseModel):\n user_uid = ModelField(name=\"user_uid\", required=True, kind=str)\n message = ModelField(name=\"message\", required=True, kind=str)\n html = ModelField(name=\"html\", required=False, kind=str)\n+\n+ async def get_user(self)->UserModel:\n+ return await self.app.services.user.get(uid=self[\"user_uid\"])\n+ \n+ async def get_channel(self):\n+ return await self.app.services.channel.get(uid=self[\"channel_uid\"])\n\\ No newline at end of file\ndiff --git a/src/snek/model/user.py b/src/snek/model/user.py\nindex 54b6f30..0621ecf 100644\n--- a/src/snek/model/user.py\n+++ b/src/snek/model/user.py\n@@ -31,3 +31,7 @@ class UserModel(BaseModel):\n password = ModelField(name=\"password\", required=True, min_length=1)\n \n last_ping = ModelField(name=\"last_ping\", required=False, kind=str)\n+\n+ async def get_channel_members(self):\n+ async for channel_member in self.app.services.channel_member.find(user_uid=self['uid'],is_banned=False,deleted_at=None):\n+ yield channel_member\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 6041901..f8a87c8 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -32,9 +32,9 @@ class NotificationService(BaseService):\n user = await self.services.user.get(uid=channel_message[\"user_uid\"])\n async for channel_member in self.services.channel_member.find(\n channel_uid=channel_message[\"channel_uid\"],\n+ is_banned=False,\n+ is_muted=False,\n+ deleted_at=None,\n ):\n if not channel_member['new_count']:\n channel_member['new_count'] = 0\ndiff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py\nindex e57b0fa..e7415e1 100644\n--- a/src/snek/system/mapper.py\n+++ b/src/snek/system/mapper.py\n@@ -20,7 +20,7 @@ class BaseMapper:\n return self.app.db\n \n async def new(self):\n- return self.model_class(mapper=self)\n+ return self.model_class(mapper=self, app=self.app)\n \n @property\n def table(self):\ndiff --git a/src/snek/system/model.py b/src/snek/system/model.py\nindex ba3fc45..1aef4ae 100644\n--- a/src/snek/system/model.py\n+++ b/src/snek/system/model.py\n@@ -270,7 +270,8 @@ class BaseModel:\n return self\n \n def __init__(self, *args, **kwargs):\n- self._mapper = None\n+ self._mapper = kwargs.get(\"mapper\")\n+ self.app = kwargs.get(\"app\")\n self.fields = {}\n for key in dir(self.__class__):\n obj = getattr(self.__class__, key)\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 185d22e..60109de 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -26,7 +26,7 @@\n \ud83c\udfe0\n \ud83d\udd0d\n- \ud83d\udc65\n+ \ud83d\udc65\n \ud83d\udd12\n "} +{"repo": ".", "date": "2025-03-08", "line": "fix: Prevent race condition when reconnecting socket", "commit": "e3afc1ba6e97378688027a60d6d98cc19a519a8c", "diff": "commit e3afc1ba6e97378688027a60d6d98cc19a519a8c\nAuthor: retoor \nDate: Sat Mar 8 03:51:54 2025 +0100\n\n Fix socket issue.\n\ndiff --git a/src/snek/static/app.js b/src/snek/static/app.js\nindex b04e8fb..f26919e 100644\n--- a/src/snek/static/app.js\n+++ b/src/snek/static/app.js\n@@ -183,8 +183,9 @@ class Socket extends EventHandler {\n const me = this \n if (this.isConnected || this.isConnecting) {\n return new Promise((resolve) => {\n+ if(me.isConnected)resolve(me)\n+ else if(me.isConnecting)\n me.connectPromises.push(resolve);\n- if (!me.isConnecting) resolve(me);\n });\n }\n this.isConnecting = true;"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Improve thread display with consistent avatar styling", "commit": "37f6725f2f7e36ec03416f191c9d16cd864991ea", "diff": "commit 37f6725f2f7e36ec03416f191c9d16cd864991ea\nAuthor: retoor \nDate: Sat Mar 8 04:07:21 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nindex 2d7d01b..8131224 100644\n--- a/src/snek/templates/threads.html\n+++ b/src/snek/templates/threads.html\n@@ -11,7 +11,7 @@\n
\n-
\n
\n
{{thread.last_message_user_nick}}
"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Display user avatars in threads", "commit": "095be5892db198d0a6356c8700ed0c038e419a29", "diff": "commit 095be5892db198d0a6356c8700ed0c038e419a29\nAuthor: retoor \nDate: Sat Mar 8 04:10:05 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nindex 8131224..bb54f4d 100644\n--- a/src/snek/templates/threads.html\n+++ b/src/snek/templates/threads.html\n@@ -11,7 +11,7 @@\n
\n-
\n
\n
{{thread.last_message_user_nick}}
"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Improve thread avatar visibility", "commit": "9292e3b8f3b64084d6bcc0b13dd42d015f4799d9", "diff": "commit 9292e3b8f3b64084d6bcc0b13dd42d015f4799d9\nAuthor: retoor \nDate: Sat Mar 8 04:11:34 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nindex bb54f4d..4141e23 100644\n--- a/src/snek/templates/threads.html\n+++ b/src/snek/templates/threads.html\n@@ -11,7 +11,7 @@\n
\n-
\n
\n
{{thread.last_message_user_nick}}
"} +{"repo": ".", "date": "2025-03-08", "line": "fix: Improve thread display and opacity", "commit": "24260f9c371ab2d989441e391f513f6460eaa1ec", "diff": "commit 24260f9c371ab2d989441e391f513f6460eaa1ec\nAuthor: retoor \nDate: Sat Mar 8 04:24:39 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nindex 4141e23..8ec781f 100644\n--- a/src/snek/templates/threads.html\n+++ b/src/snek/templates/threads.html\n@@ -2,9 +2,6 @@\n \n {% block main %}\n
\n-
\n-

?

\n-
\n
\n {% for thread in threads %}\n {% autoescape false %}\n@@ -14,10 +11,10 @@\n
\n
\n-
{{thread.last_message_user_nick}}
\n+
{{thread.last_message_user_nick}}
\n
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ thread.last_message_text }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{%\n endautoescape %}
\n-
\n+
\n
\n
"} +{"repo": ".", "date": "2025-03-08", "line": "refactor: Unified styling for chat messages and threads", "commit": "1b72063a5b972dd726c647b7397f0ced16bd66c2", "diff": "commit 1b72063a5b972dd726c647b7397f0ced16bd66c2\nAuthor: retoor \nDate: Sat Mar 8 07:22:14 2025 +0100\n\n Channel list.\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex e42784b..4405785 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -102,7 +102,7 @@ a {\n overflow-y: auto;\n }\n \n-.chat-messages {\n+.chat-messages, .threads {\n flex: 1;\n overflow-y: scroll;\n scrollbar-width: none;\n@@ -130,7 +130,7 @@ a {\n display: none;\n }\n \n-.chat-messages .message {\n+.chat-messages .message, .threads .thread {\n display: flex;\n align-items: flex-start;\n margin-bottom: 0;\n@@ -138,7 +138,7 @@ a {\n border-radius: 8px;\n }\n \n-.chat-messages .message .avatar {\n+.chat-messages .message .avatar, .threads .thread .avatar {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n@@ -152,11 +152,11 @@ a {\n margin-right: 15px;\n }\n \n-.chat-messages .message .message-content {\n+.chat-messages .message .message-content, .threads .thread .message-content {\n flex: 1;\n }\n \n-.chat-messages .message .message-content .author {\n+.chat-messages .message .message-content .author, .threads .thread .message-content .author {\n font-weight: bold;\n margin-bottom: 3px;\n@@ -172,7 +172,7 @@ word-break: break-word;\n overflow-wrap: break-word;\n hyphens: auto;\n }\n-.chat-messages .message .message-content .text {\n+.chat-messages .message .message-content .text, .threads .thread .message-content .text {\n margin-bottom: 5px;\n word-break: break-word;\n@@ -191,7 +191,7 @@ hyphens: auto;\n }\n }\n \n-.chat-messages .message .message-content .time {\n+.chat-messages .message .message-content .time, .threads .thread .message-content .time {\n font-size: 0.8em;\n }\n@@ -294,7 +294,7 @@ input[type=\"text\"], .chat-input textarea {\n }\n \n-.chat-messages {\n+.chat-messages, .threads {\n scrollbar-width: none;\n }\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nindex 8ec781f..b982914 100644\n--- a/src/snek/templates/threads.html\n+++ b/src/snek/templates/threads.html\n@@ -2,12 +2,12 @@\n \n {% block main %}\n
\n-
\n+
\n {% for thread in threads %}\n {% autoescape false %}\n
\n+ class=\"thread\">\n
\n
\n@@ -32,71 +32,11 @@\n });\n }\n \n- function isElementVisible(element) {\n- const rect = element.getBoundingClientRect();\n- return (\n- rect.top >= 0 &&\n- rect.left >= 0 &&\n- rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n- rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n- );\n- }\n-\n- const messagesContainer = document.querySelector(\".chat-messages\");\n-\n- function isScrolledPastHalf() {\n- let scrollTop = messagesContainer.scrollTop;\n- let scrollableHeight = messagesContainer.scrollHeight - messagesContainer.clientHeight;\n-\n- if (scrollTop < scrollableHeight / 2) {\n- return true;\n- }\n- return false;\n- }\n \n- let isLoadingExtra = false;\n-\n- async function loadExtra() {\n- const firstMessage = messagesContainer.querySelector(\".message:first-child\");\n- if (isLoadingExtra) {\n- return;\n- }\n- if (!isScrolledPastHalf()) {\n- return;\n- }\n-\n- isLoadingExtra = true;\n-\n- const messages = await app.rpc.getMessages(channelUid, 0, firstMessage.dataset.created_at);\n-\n- messages.forEach((message) => {\n- firstMessage.insertAdjacentHTML(\"beforebegin\", message.html);\n- })\n- updateLayout(false);\n-\n- isLoadingExtra = false;\n- }\n-\n- messagesContainer.addEventListener(\"scroll\", () => {\n- loadExtra();\n- });\n+ const threadsContainer = document.querySelector(\".chat-threads\");\n \n function updateLayout(doScrollDown) {\n- const messagesContainer = document.querySelector(\".chat-messages\");\n updateTimes();\n- let previousUser = null;\n- document.querySelectorAll(\".message\").forEach((message) => {\n- if (previousUser !== message.dataset.user_uid) {\n- message.classList.add(\"switch-user\");\n- previousUser = message.dataset.user_uid;\n- } else {\n- message.classList.remove(\"switch-user\");\n- }\n- });\n- lastMessage = messagesContainer.querySelector(\".message:last-child\");\n- if (doScrollDown) {\n- lastMessage.scrollIntoView({ inline: \"nearest\" });\n- }\n }\n \n setInterval(updateTimes, 1000);\n@@ -131,20 +71,17 @@\n }\n }\n \n- const messagesContainer = document.querySelector(\".chat-messages\");\n- const lastMessage = messagesContainer.querySelector(\".message:last-child\");\n- const doScrollDownBecauseLastMessageIsVisible = !lastMessage || isElementVisible(lastMessage);\n+ const threadsContainer = document.querySelector(\".chat-threads\");\n \n const message = document.createElement(\"div\");\n message.innerHTML = data.html;\n- document.querySelector(\".chat-messages\").appendChild(message.firstChild);\n- updateLayout(doScrollDownBecauseLastMessageIsVisible);\n+ document.querySelector(\".chat-threads\").appendChild(message.firstChild);\n+ updateLayout();\n setTimeout(() => {\n- updateLayout(doScrollDownBecauseLastMessageIsVisible)\n+ updateLayout()\n }, 1000);\n });\n \n- initInputField(document.querySelector(\"textarea\"));\n updateLayout(true);\n \n {% endblock %}"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Improved thread display and DM channel naming\n\nfix: Corrected DM channel retrieval and added name color for DM threads", "commit": "5a72c8cd83d1374ff709556cbf4b4ddbf7a9ab7a", "diff": "commit 5a72c8cd83d1374ff709556cbf4b4ddbf7a9ab7a\nAuthor: retoor \nDate: Sat Mar 8 08:25:49 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/model/channel_member.py b/src/snek/model/channel_member.py\nindex 5a8332e..9689fa5 100644\n--- a/src/snek/model/channel_member.py\n+++ b/src/snek/model/channel_member.py\n@@ -22,11 +22,11 @@ class ChannelMemberModel(BaseModel):\n return await self.app.services.channel.get(uid=self['channel_uid'])\n \n async def get_name(self):\n- if self[\"channel_uid\"] == \"dm\":\n+ channel = await self.get_channel()\n+ if channel[\"tag\"] == \"dm\":\n user = await self.get_other_dm_user()\n return user['nick']\n- channel = await self.get_channel()\n- return channel['name']\n+ return channel['name'] or self['label']\n \n async def get_other_dm_user(self):\n channel = await self.get_channel()\n@@ -37,4 +37,4 @@ class ChannelMemberModel(BaseModel):\n if model[\"uid\"] != self['uid']:\n return await self.app.services.user.get(uid=model[\"user_uid\"])\n return await self.get_user()\n- \n\\ No newline at end of file\n+ \ndiff --git a/src/snek/service/channel_member.py b/src/snek/service/channel_member.py\nindex 191a063..42415d1 100644\n--- a/src/snek/service/channel_member.py\n+++ b/src/snek/service/channel_member.py\n@@ -36,7 +36,11 @@ class ChannelMemberService(BaseService):\n async def get_dm(self,from_user, to_user):\n 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)):\n return model\n- return None \n+ if not from_user == to_user:\n+ return None \n+ 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)):\n+ \n+ return model \n \n async def get_other_dm_user(self, channel_uid, user_uid):\n channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)\ndiff --git a/src/snek/templates/threads.html b/src/snek/templates/threads.html\nindex b982914..73c256a 100644\n--- a/src/snek/templates/threads.html\n+++ b/src/snek/templates/threads.html\n@@ -5,18 +5,18 @@\n
\n {% for thread in threads %}\n {% autoescape false %}\n-
\n
\n
\n-
{{thread.last_message_user_nick}}
\n+
{{thread.name}}
\n
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ thread.last_message_text }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{%\n endautoescape %}
\n
\n
\n-
\n+ \n \n {% endautoescape %}\n {% endfor %}\ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex bdcddae..2c48cb4 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -3,7 +3,7 @@\n {% block main %}\n
\n
\n-

{{ channel.label.value }}

\n+

{{ name }}

\n
\n
\n {% for message in messages %}\ndiff --git a/src/snek/view/threads.py b/src/snek/view/threads.py\nindex d8f4359..772685f 100644\n--- a/src/snek/view/threads.py\n+++ b/src/snek/view/threads.py\n@@ -8,23 +8,24 @@ class ThreadsView(BaseView):\n async for channel_member in user.get_channel_members():\n thread = {}\n channel = await self.services.channel.get(uid=channel_member[\"channel_uid\"])\n+ last_message = await channel.get_last_message()\n+ if not last_message:\n+ continue\n+\n thread[\"uid\"] = channel['uid']\n thread[\"name\"] = await channel_member.get_name()\n thread[\"new_count\"] = channel_member[\"new_count\"]\n thread[\"last_message_on\"] = channel[\"last_message_on\"]\n thread['created_at'] = thread['last_message_on']\n- last_message = await channel.get_last_message()\n- if last_message:\n- thread[\"last_message_text\"] = last_message[\"message\"]\n- thread['last_message_user_uid'] = last_message[\"user_uid\"]\n- user_last_message = await self.app.services.user.get(uid=last_message[\"user_uid\"])\n- thread['last_message_user_nick'] = user_last_message[\"nick\"]\n- thread['last_message_user_color'] = user_last_message['color']\n- else:\n- thread[\"last_message_text\"] = None \n- thread['last_message_user_uid'] = None \n- thread['last_message_user_nick'] = None \n- thread['last_message_user_color'] = None\n+\n+ \n+ thread[\"last_message_text\"] = last_message[\"message\"]\n+ thread['last_message_user_uid'] = last_message[\"user_uid\"]\n+ user_last_message = await self.app.services.user.get(uid=last_message[\"user_uid\"])\n+ if channel['tag'] == \"dm\":\n+ thread['name_color'] = user_last_message['color']\n+ thread['last_message_user_color'] = user_last_message['color'] \n threads.append(thread)\n \n \ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex 8fd5ddc..3ae7902 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -42,6 +42,11 @@ class WebView(BaseView):\n return web.HTTPFound(\"/channel/{}.html\".format(channel[\"uid\"]))\n if not channel:\n return web.HTTPNotFound()\n+ \n+ channel_member = await self.app.services.channel_member.get(user_uid=self.session.get(\"uid\"), channel_uid=channel[\"uid\"])\n+ if not channel_member:\n+ return web.HTTPNotFound()\n+\n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n messages = [await self.app.services.channel_message.to_extended_dict(message) for message in await self.app.services.channel_message.offset(\n channel[\"uid\"]\n@@ -60,4 +65,6 @@ class WebView(BaseView):\n item[\"name\"] = subscribed_channel[\"label\"]\n item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n channels.append(item)\n- return await self.render_template(\"web.html\", {\"channel\": channel,\"user\": user,\"messages\": messages , \"channels\": channels})\n\\ No newline at end of file\n+\n+ name = await channel_member.get_name()\n+ return await self.render_template(\"web.html\", {\"name\": name, \"channel\": channel,\"user\": user,\"messages\": messages , \"channels\": channels})"} +{"repo": ".", "date": "2025-03-08", "line": "fix: Round follow-up messages corners", "commit": "98c2213a862b253f8f967f428b0b248bbe3a32f7", "diff": "commit 98c2213a862b253f8f967f428b0b248bbe3a32f7\nAuthor: BordedDev <>\nDate: Sat Mar 8 17:25:56 2025 +0100\n\n Fix rounded corners on follow-up messages\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex 4405785..bcdc1d1 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -1,183 +1,189 @@\n * {\n- margin: 0;\n- box-sizing: border-box;\n+ margin: 0;\n+ box-sizing: border-box;\n }\n \n .gallery {\n- padding: 50px;\n- height: auto;\n- overflow: auto;\n- flex: 1;\n+ padding: 50px;\n+ height: auto;\n+ overflow: auto;\n+ flex: 1;\n }\n \n .gallery.tile, .tile {\n- width: 100px;\n- height: 100px;\n- object-fit: cover;\n- margin: 20px 10px 20px 0;\n- border-radius: 5px;\n+ width: 100px;\n+ height: 100px;\n+ object-fit: cover;\n+ margin: 20px 10px 20px 0;\n+ border-radius: 5px;\n }\n \n body {\n- font-family: Arial, sans-serif;\n- line-height: 1.5;\n- display: flex;\n- flex-direction: column;\n- height: 100vh;\n- min-width: 100%;\n+ font-family: Arial, sans-serif;\n+ line-height: 1.5;\n+ display: flex;\n+ flex-direction: column;\n+ height: 100vh;\n+ min-width: 100%;\n }\n \n main {\n- display: flex;\n- flex: 1;\n- overflow: hidden;\n+ display: flex;\n+ flex: 1;\n+ overflow: hidden;\n }\n \n header {\n- padding: 10px 20px;\n- display: flex;\n- justify-content: space-between;\n- align-items: center;\n+ padding: 10px 20px;\n+ display: flex;\n+ justify-content: space-between;\n+ align-items: center;\n }\n \n header .logo {\n- font-size: 1.5em;\n- font-weight: bold;\n+ font-size: 1.5em;\n+ font-weight: bold;\n }\n \n header nav a {\n- text-decoration: none;\n- margin-left: 15px;\n- font-size: 1em;\n- transition: color 0.3s;\n+ text-decoration: none;\n+ margin-left: 15px;\n+ font-size: 1em;\n+ transition: color 0.3s;\n }\n \n .no-select {\n- -webkit-user-select: none; \n- -moz-user-select: none; \n- -ms-user-select: none; \n- user-select: none; \n+ -webkit-user-select: none;\n+ -moz-user-select: none;\n+ -ms-user-select: none;\n+ user-select: none;\n }\n \n header nav a:hover {\n }\n \n a {\n- font-weight: bold;\n- margin-bottom: 3px;\n+ font-weight: bold;\n+ margin-bottom: 3px;\n }\n \n .chat-area {\n- flex: 1;\n- display: flex;\n- flex-direction: column;\n- overflow: hidden;\n+ flex: 1;\n+ display: flex;\n+ flex-direction: column;\n+ overflow: hidden;\n }\n \n .chat-header {\n- padding: 10px 20px;\n- user-select: none;\n+ padding: 10px 20px;\n+ user-select: none;\n }\n \n .chat-header h2 {\n- font-size: 1.2em;\n+ font-size: 1.2em;\n \n }\n \n .message-list {\n- flex: 1;\n- height: 200px;\n- padding-bottom: 40px;\n- overflow-y: auto;\n+ flex: 1;\n+ height: 200px;\n+ padding-bottom: 40px;\n+ overflow-y: auto;\n }\n \n .chat-messages, .threads {\n- flex: 1;\n- overflow-y: scroll;\n- scrollbar-width: none;\n- -ms-overflow-style: none;\n- padding: 10px;\n- height: 200px;\n+ flex: 1;\n+ overflow-y: scroll;\n+ scrollbar-width: none;\n+ -ms-overflow-style: none;\n+ padding: 10px;\n+ height: 200px;\n }\n+\n .container {\n- flex: 1;\n- padding: 10px;\n- ul {\n- list-style: none;\n- margin: 0;\n- padding: 0;\n- }\n- a {\n+ flex: 1;\n+ padding: 10px;\n+\n+ ul {\n+ list-style: none;\n+ margin: 0;\n+ padding: 0;\n+ }\n+\n+ a {\n font-size: 20px;\n- }\n- \n+ }\n+\n }\n \n .chat-messages::-webkit-scrollbar {\n- display: none;\n+ display: none;\n }\n \n .chat-messages .message, .threads .thread {\n- display: flex;\n- align-items: flex-start;\n- margin-bottom: 0;\n- padding: 5px;\n- border-radius: 8px;\n+ display: flex;\n+ align-items: flex-start;\n+ margin-bottom: 0;\n+ padding: 5px;\n+ border-radius: 8px;\n }\n \n .chat-messages .message .avatar, .threads .thread .avatar {\n- width: 40px;\n- height: 40px;\n- border-radius: 50%;\n- font-size: 1em;\n- font-weight: bold;\n- display: flex;\n- justify-content: center;\n- align-items: center;\n- margin-right: 15px;\n+ width: 40px;\n+ height: 40px;\n+ border-radius: 50%;\n+ font-size: 1em;\n+ font-weight: bold;\n+ display: flex;\n+ justify-content: center;\n+ align-items: center;\n+ margin-right: 15px;\n }\n \n .chat-messages .message .message-content, .threads .thread .message-content {\n- flex: 1;\n+ flex: 1;\n }\n \n .chat-messages .message .message-content .author, .threads .thread .message-content .author {\n- font-weight: bold;\n- margin-bottom: 3px;\n+ font-weight: bold;\n+ margin-bottom: 3px;\n }\n+\n * {\n-word-break: break-word;\n- overflow-wrap: break-word;\n+ word-break: break-word;\n+ overflow-wrap: break-word;\n hyphens: auto;\n }\n+\n .highlight pre {\n white-space: pre-wrap;\n word-break: break-word;\n overflow-wrap: break-word;\n hyphens: auto;\n- }\n+}\n+\n .chat-messages .message .message-content .text, .threads .thread .message-content .text {\n- margin-bottom: 5px;\n- word-break: break-word;\n- overflow-wrap: break-word;\n-hyphens: auto; \n+ margin-bottom: 5px;\n+ word-break: break-word;\n+ overflow-wrap: break-word;\n+ hyphens: auto;\n }\n \n .message-content {\n@@ -187,151 +193,153 @@ hyphens: auto;\n .message-content {\n \n img, video, iframe, div {\n- max-width: 100%; \n+ max-width: 90%;\n+ border-radius: 20px;\n }\n }\n \n .chat-messages .message .message-content .time, .threads .thread .message-content .time {\n- font-size: 0.8em;\n+ font-size: 0.8em;\n }\n \n .chat-input {\n- padding: 15px;\n- display: flex;\n- align-items: center;\n+ padding: 15px;\n+ display: flex;\n+ align-items: center;\n }\n \n input[type=\"text\"], .chat-input textarea {\n- flex: 1;\n- color: white;\n- border: none;\n- padding: 10px;\n- border-radius: 5px;\n- resize: none;\n+ flex: 1;\n+ color: white;\n+ border: none;\n+ padding: 10px;\n+ border-radius: 5px;\n+ resize: none;\n }\n \n .chat-input upload-button {\n- color: white;\n- border: none;\n- padding: 10px 15px;\n- margin-left: 10px;\n- border-radius: 5px;\n- cursor: pointer;\n- font-size: 1em;\n- transition: background-color 0.3s;\n+ color: white;\n+ border: none;\n+ padding: 10px 15px;\n+ margin-left: 10px;\n+ border-radius: 5px;\n+ cursor: pointer;\n+ font-size: 1em;\n+ transition: background-color 0.3s;\n }\n \n .chat-input button:hover {\n }\n \n @media (max-width: 768px) {\n- .sidebar {\n- display: none;\n- }\n+ .sidebar {\n+ display: none;\n+ }\n }\n \n .message {\n- .text {\n- max-width: 100%;\n- word-wrap: break-word;\n- overflow-wrap: break-word;\n- hyphens: auto;\n- }\n+ .text {\n+ max-width: 100%;\n+ word-wrap: break-word;\n+ overflow-wrap: break-word;\n+ hyphens: auto;\n+ }\n \n- .avatar {\n- opacity: 0;\n- }\n+ .avatar {\n+ opacity: 0;\n+ }\n \n- .author, .time {\n- display: none;\n- }\n+ .author, .time {\n+ display: none;\n+ }\n }\n \n .message.switch-user {\n- .text img,iframe, video {\n- max-width: 90%;\n- border-radius: 20px;\n- }\n- \n- .avatar {\n- user-select: none;\n- opacity: 1;\n- }\n+ .text img, iframe, video {\n+ max-width: 90%;\n+ border-radius: 20px;\n+ }\n \n- .author {\n- display: block;\n- }\n+ .avatar {\n+ user-select: none;\n+ opacity: 1;\n+ }\n+\n+ .author {\n+ display: block;\n+ }\n }\n \n-.message:has(+ .message.switch-user), .message:last-child{ \n+.message:has(+ .message.switch-user), .message:last-child {\n .time {\n- display: block;\n-}\n+ display: block;\n+ }\n }\n \n ::-webkit-scrollbar {\n- display:none;\n+ display: none;\n }\n \n ::-webkit-scrollbar-track {\n }\n \n ::-webkit-scrollbar-thumb {\n- border-radius: 3px;\n+ border-radius: 3px;\n }\n \n ::-webkit-scrollbar-thumb:hover {\n }\n \n .chat-messages, .threads {\n- scrollbar-width: none;\n+ scrollbar-width: none;\n }\n \n a {\n- text-decoration:none\n+ text-decoration: none\n }\n+\n .sidebar {\n- width: 250px;\n- padding: 20px;\n- overflow-y: auto;\n+ width: 250px;\n+ padding: 20px;\n+ overflow-y: auto;\n }\n \n .sidebar h2 {\n- font-size: 1.2em;\n- margin-bottom: 20px;\n+ font-size: 1.2em;\n+ margin-bottom: 20px;\n }\n \n .sidebar ul {\n- list-style: none;\n+ list-style: none;\n }\n \n .sidebar ul li {\n- margin-bottom: 15px;\n+ margin-bottom: 15px;\n }\n \n .sidebar ul li a {\n- text-decoration: none;\n- font-size: 1em;\n- transition: color 0.3s;\n+ text-decoration: none;\n+ font-size: 1em;\n+ transition: color 0.3s;\n }\n \n .sidebar ul li a:hover {\n }"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Improved user search display with thread-like layout and real-time updates", "commit": "0a9b66d2f76a2c4418db7149b17729bf8a2dc811", "diff": "commit 0a9b66d2f76a2c4418db7149b17729bf8a2dc811\nAuthor: retoor \nDate: Sat Mar 8 17:33:17 2025 +0100\n\n Updated search.\n\ndiff --git a/src/snek/templates/search_user.html b/src/snek/templates/search_user.html\nindex b6a0439..842b982 100644\n--- a/src/snek/templates/search_user.html\n+++ b/src/snek/templates/search_user.html\n@@ -13,16 +13,90 @@\n \n \n \n- \n \n \n+ {% endfor %}\n+
\n+\n
\n
\n-{% endblock %}\n\\ No newline at end of file\n+\n+\n+\n+{% endblock %}\ndiff --git a/src/snek/view/search_user.py b/src/snek/view/search_user.py\nindex c28b883..347d3b2 100644\n--- a/src/snek/view/search_user.py\n+++ b/src/snek/view/search_user.py\n@@ -40,14 +40,14 @@ class SearchUserView(BaseFormView):\n users = []\n query = self.request.query.get(\"query\")\n if query:\n- users = await self.app.services.user.search(query)\n+ users = [user.record for user in await self.app.services.user.search(query)]\n \n if self.request.path.endswith(\".json\"):\n return await super().get()\n-\n- return await self.render_template(\"search_user.html\", {\"users\": users, \"query\": query or ''})\n+ current_user = await self.app.services.user.get(uid=self.session.get(\"uid\"))\n+ return await self.render_template(\"search_user.html\", {\"users\": users, \"query\": query or '','current_user': current_user})\n \n async def submit(self, form):\n if await form.is_valid:\n return {\"redirect_url\": \"/search-user.html?query=\" + form['username']}\n- return {\"is_valid\": False}\n\\ No newline at end of file\n+ return {\"is_valid\": False}"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Set default page titles and update login/register titles", "commit": "6ecd356cc08e17596ff6b5007c46def2bc17c851", "diff": "commit 6ecd356cc08e17596ff6b5007c46def2bc17c851\nAuthor: BordedDev <>\nDate: Sat Mar 8 18:04:53 2025 +0100\n\n Made improvement to page titles (mainly setting a default, login and regsiter)\n\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex 36a012c..0785366 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -1,26 +1,25 @@\n \n \n \n- \n- \n- {% block title %}{% endblock %}\n- \n- \n- \n- \n- \n+ \n+ \n+ {% block title %}Snek chat by Molodetz{% endblock %}\n+ \n+ \n+ \n+ \n+ \n \n \n- \n- \n+ \n \n \n-
\n- {% block header %}\n- {% endblock %}\n+
\n+ {% block header %}\n+ {% endblock %}\n \n-
\n-
\n+
\n+
\n {% block main %}\n {% endblock %}\n
\ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex 0a6bcc1..0ffc379 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -1,7 +1,11 @@\n {% extends \"base.html\" %}\n \n+{% block title %}\n+ Login - Snek chat by Molodetz\n+{% endblock %}\n+\n {% block main %}\n- \n- \n+ \n+ \n \n {% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex f8d1067..2b783a7 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -1,7 +1,11 @@\n {% extends \"base.html\" %}\n \n+{% block title %}\n+ Register - Snek chat by Molodetz\n+{% endblock %}\n+\n {% block main %}\n-\n- \n- \n+ \n+\n+ \n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-03-08", "line": "fix: Improved form submission and change event handling", "commit": "804556b74d8caa5e3a79a03cd1a8d7870843b898", "diff": "commit 804556b74d8caa5e3a79a03cd1a8d7870843b898\nAuthor: BordedDev <>\nDate: Sat Mar 8 18:09:14 2025 +0100\n\n Fixed issues with auto complete not working correctly with form sync\n\ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nindex 730d70a..e71d817 100644\n--- a/src/snek/static/generic-form.js\n+++ b/src/snek/static/generic-form.js\n@@ -62,7 +62,7 @@ class GenericField extends HTMLElement {\n \n constructor() {\n super();\n- this.attachShadow({ mode: 'open' });\n+ this.attachShadow({mode: 'open'});\n this.container = document.createElement('div');\n this.styleElement = document.createElement('style');\n this.styleElement.innerHTML = `\n@@ -165,18 +165,26 @@ class GenericField extends HTMLElement {\n const me = this;\n this.inputElement.addEventListener(\"keyup\", (e) => {\n if (e.key === 'Enter') {\n+ const event = new CustomEvent(\"change\", {detail: me, bubbles: true});\n+ me.dispatchEvent(event);\n+\n me.dispatchEvent(new Event(\"submit\"));\n } else if (me.field.value !== e.target.value) {\n- const event = new CustomEvent(\"change\", { detail: me, bubbles: true });\n+ const event = new CustomEvent(\"change\", {detail: me, bubbles: true});\n me.dispatchEvent(event);\n }\n });\n \n this.inputElement.addEventListener(\"click\", (e) => {\n- const event = new CustomEvent(\"click\", { detail: me, bubbles: true });\n+ const event = new CustomEvent(\"click\", {detail: me, bubbles: true});\n me.dispatchEvent(event);\n });\n \n+ this.inputElement.addEventListener(\"blur\", (e) => {\n+ const event = new CustomEvent(\"change\", {detail: me, bubbles: true});\n+ me.dispatchEvent(event);\n+ }, true);\n+\n this.container.appendChild(this.inputElement);\n }\n \n@@ -226,7 +234,7 @@ class GenericForm extends HTMLElement {\n \n constructor() {\n super();\n- this.attachShadow({ mode: 'open' });\n+ this.attachShadow({mode: 'open'});\n this.styleElement = document.createElement(\"style\");\n this.styleElement.innerHTML = `\n \n@@ -307,6 +315,16 @@ class GenericForm extends HTMLElement {\n }\n }\n });\n+\n+ fieldElement.addEventListener(\"submit\", async (e) => {\n+ const isValid = await this.validate();\n+ if (isValid) {\n+ const saveResult = await this.submit();\n+ if (saveResult.redirect_url) {\n+ window.location.pathname = saveResult.redirect_url;\n+ }\n+ }\n+ })\n });\n \n } catch (error) {\n@@ -322,7 +340,7 @@ class GenericForm extends HTMLElement {\n headers: {\n 'Content-Type': 'application/json'\n },\n- body: JSON.stringify({ \"action\": \"validate\", \"form\": this.form })\n+ body: JSON.stringify({\"action\": \"validate\", \"form\": this.form})\n });\n \n const form = await response.json();\n@@ -353,7 +371,7 @@ class GenericForm extends HTMLElement {\n headers: {\n 'Content-Type': 'application/json'\n },\n- body: JSON.stringify({ \"action\": \"submit\", \"form\": this.form })\n+ body: JSON.stringify({\"action\": \"submit\", \"form\": this.form})\n });\n return await response.json();\n }"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Added meta information to base.html", "commit": "7c22a70722db3fa97a813c30c01c4cd5462138eb", "diff": "commit 7c22a70722db3fa97a813c30c01c4cd5462138eb\nAuthor: BordedDev <>\nDate: Sat Mar 8 18:25:15 2025 +0100\n\n Added additional meta info to base.html\n\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex 0785366..1e89c50 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -3,7 +3,15 @@\n \n \n \n+ \n+ \n+ \n+ \n+ \n+\n {% block title %}Snek chat by Molodetz{% endblock %}\n+\n \n \n "} +{"repo": ".", "date": "2025-03-08", "line": "feat: Sort threads by last message timestamp", "commit": "8e195a49e3e914a4b241e95378bd9a07611715a8", "diff": "commit 8e195a49e3e914a4b241e95378bd9a07611715a8\nAuthor: retoor \nDate: Sat Mar 8 18:41:37 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/view/threads.py b/src/snek/view/threads.py\nindex 772685f..14431f1 100644\n--- a/src/snek/view/threads.py\n+++ b/src/snek/view/threads.py\n@@ -27,6 +27,7 @@ class ThreadsView(BaseView):\n thread['name_color'] = user_last_message['color']\n thread['last_message_user_color'] = user_last_message['color'] \n threads.append(thread)\n-\n+ \n+ threads.sort(key=lambda x: x['last_message_on'], reverse=True)\n \n return await self.render_template(\"threads.html\", dict(threads=threads,user=user))"} +{"repo": ".", "date": "2025-03-08", "line": "style: Improved form input focus styling", "commit": "c9113ca09500c5b3cc277fb09b9607a505d39f30", "diff": "commit c9113ca09500c5b3cc277fb09b9607a505d39f30\nAuthor: BordedDev <>\nDate: Sat Mar 8 18:56:55 2025 +0100\n\n Tweaked form input styling\n\ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nindex e71d817..11647dc 100644\n--- a/src/snek/static/generic-form.js\n+++ b/src/snek/static/generic-form.js\n@@ -75,16 +75,28 @@ class GenericField extends HTMLElement {\n }\n \n input {\n- width: 90%;\n- padding: 10px;\n- margin: 10px 0;\n- border-radius: 5px;\n- font-size: 1em;\n+ width: 90%;\n+ padding: 10px;\n+ margin: 10px 0;\n+ border-radius: 5px;\n+ font-size: 1em;\n+ \n+ &:focus {\n+ }\n+ \n+ &::placeholder {\n+ transition: opacity 0.3s;\n+ }\n+ \n+ &:focus::placeholder {\n+ opacity: 0.4;\n+ }\n }\n-\n+ \n button {\n width: 50%;\n padding: 10px;"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Added head block for custom HTML head content", "commit": "62aa15a4b4d6514824378cca73084c9ce2df903b", "diff": "commit 62aa15a4b4d6514824378cca73084c9ce2df903b\nAuthor: BordedDev <>\nDate: Sat Mar 8 18:59:00 2025 +0100\n\n Added head block\n\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex 1e89c50..d93b568 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -20,6 +20,9 @@\n \n \n \n+\n+ {% block head %}\n+ {% endblock %}\n \n \n
"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Implemented back button styling and layout", "commit": "ad7eab9717848584369ded6e19babb6a7b9f5b98", "diff": "commit ad7eab9717848584369ded6e19babb6a7b9f5b98\nAuthor: BordedDev <>\nDate: Sat Mar 8 19:07:09 2025 +0100\n\n Tweaked the back button position\n\ndiff --git a/src/snek/static/back-form.css b/src/snek/static/back-form.css\nnew file mode 100644\nindex 0000000..4824d6c\n--- /dev/null\n+++ b/src/snek/static/back-form.css\n@@ -0,0 +1,18 @@\n+.back-form {\n+ display: grid;\n+ grid-template-columns: auto auto;\n+ grid-template-rows: auto auto;\n+\n+ fancy-button {\n+ grid-column: 1 / 1;\n+ grid-row: 1 / 1;\n+ z-index: 1;\n+ margin-left: 30px;\n+ margin-top: 30px;\n+ }\n+\n+ generic-form {\n+ grid-column: 1 / 3;\n+ grid-row: 1 / 3;\n+ }\n+}\n\\ No newline at end of file\ndiff --git a/src/snek/templates/login.html b/src/snek/templates/login.html\nindex 0ffc379..ed81224 100644\n--- a/src/snek/templates/login.html\n+++ b/src/snek/templates/login.html\n@@ -4,8 +4,13 @@\n Login - Snek chat by Molodetz\n {% endblock %}\n \n-{% block main %}\n- \n- \n+{% block head %}\n+ \n+{% endblock %}\n \n+{% block main %}\n+
\n+ \n+ \n+
\n {% endblock %}\ndiff --git a/src/snek/templates/register.html b/src/snek/templates/register.html\nindex 2b783a7..2fa89d3 100644\n--- a/src/snek/templates/register.html\n+++ b/src/snek/templates/register.html\n@@ -4,8 +4,14 @@\n Register - Snek chat by Molodetz\n {% endblock %}\n \n+{% block head %}\n+ \n+{% endblock %}\n+\n {% block main %}\n- \n+
\n+ \n \n- \n+ \n+
\n {% endblock %}\n\\ No newline at end of file"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Revert auto formatting changes", "commit": "e91ae4584aaf08cb02b89cdf26b8c43a8c8179b0", "diff": "commit e91ae4584aaf08cb02b89cdf26b8c43a8c8179b0\nAuthor: BordedDev <>\nDate: Sat Mar 8 19:35:22 2025 +0100\n\n Undo auto formatting\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex bcdc1d1..3d154a9 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -1,345 +1,345 @@\n * {\n- margin: 0;\n- box-sizing: border-box;\n+ margin: 0;\n+ box-sizing: border-box;\n }\n \n .gallery {\n- padding: 50px;\n- height: auto;\n- overflow: auto;\n- flex: 1;\n+ padding: 50px;\n+ height: auto;\n+ overflow: auto;\n+ flex: 1;\n }\n \n .gallery.tile, .tile {\n- width: 100px;\n- height: 100px;\n- object-fit: cover;\n- margin: 20px 10px 20px 0;\n- border-radius: 5px;\n+ width: 100px;\n+ height: 100px;\n+ object-fit: cover;\n+ margin: 20px 10px 20px 0;\n+ border-radius: 5px;\n }\n \n body {\n- font-family: Arial, sans-serif;\n- line-height: 1.5;\n- display: flex;\n- flex-direction: column;\n- height: 100vh;\n- min-width: 100%;\n+ font-family: Arial, sans-serif;\n+ line-height: 1.5;\n+ display: flex;\n+ flex-direction: column;\n+ height: 100vh;\n+ min-width: 100%;\n }\n \n main {\n- display: flex;\n- flex: 1;\n- overflow: hidden;\n+ display: flex;\n+ flex: 1;\n+ overflow: hidden;\n }\n \n header {\n- padding: 10px 20px;\n- display: flex;\n- justify-content: space-between;\n- align-items: center;\n+ padding: 10px 20px;\n+ display: flex;\n+ justify-content: space-between;\n+ align-items: center;\n }\n \n header .logo {\n- font-size: 1.5em;\n- font-weight: bold;\n+ font-size: 1.5em;\n+ font-weight: bold;\n }\n \n header nav a {\n- text-decoration: none;\n- margin-left: 15px;\n- font-size: 1em;\n- transition: color 0.3s;\n+ text-decoration: none;\n+ margin-left: 15px;\n+ font-size: 1em;\n+ transition: color 0.3s;\n }\n \n .no-select {\n- -webkit-user-select: none;\n- -moz-user-select: none;\n- -ms-user-select: none;\n- user-select: none;\n+ -webkit-user-select: none;\n+ -moz-user-select: none;\n+ -ms-user-select: none;\n+ user-select: none;\n }\n \n header nav a:hover {\n }\n \n a {\n- font-weight: bold;\n- margin-bottom: 3px;\n+ font-weight: bold;\n+ margin-bottom: 3px;\n }\n \n .chat-area {\n- flex: 1;\n- display: flex;\n- flex-direction: column;\n- overflow: hidden;\n+ flex: 1;\n+ display: flex;\n+ flex-direction: column;\n+ overflow: hidden;\n }\n \n .chat-header {\n- padding: 10px 20px;\n- user-select: none;\n+ padding: 10px 20px;\n+ user-select: none;\n }\n \n .chat-header h2 {\n- font-size: 1.2em;\n+ font-size: 1.2em;\n \n }\n \n .message-list {\n- flex: 1;\n- height: 200px;\n- padding-bottom: 40px;\n- overflow-y: auto;\n+ flex: 1;\n+ height: 200px;\n+ padding-bottom: 40px;\n+ overflow-y: auto;\n }\n \n .chat-messages, .threads {\n- flex: 1;\n- overflow-y: scroll;\n- scrollbar-width: none;\n- -ms-overflow-style: none;\n- padding: 10px;\n- height: 200px;\n+ flex: 1;\n+ overflow-y: scroll;\n+ scrollbar-width: none;\n+ -ms-overflow-style: none;\n+ padding: 10px;\n+ height: 200px;\n }\n \n .container {\n- flex: 1;\n- padding: 10px;\n+ flex: 1;\n+ padding: 10px;\n \n- ul {\n- list-style: none;\n- margin: 0;\n- padding: 0;\n- }\n+ ul {\n+ list-style: none;\n+ margin: 0;\n+ padding: 0;\n+ }\n \n- a {\n- font-size: 20px;\n- }\n+ a {\n+ font-size: 20px;\n+ }\n \n }\n \n .chat-messages::-webkit-scrollbar {\n- display: none;\n+ display: none;\n }\n \n .chat-messages .message, .threads .thread {\n- display: flex;\n- align-items: flex-start;\n- margin-bottom: 0;\n- padding: 5px;\n- border-radius: 8px;\n+ display: flex;\n+ align-items: flex-start;\n+ margin-bottom: 0;\n+ padding: 5px;\n+ border-radius: 8px;\n }\n \n .chat-messages .message .avatar, .threads .thread .avatar {\n- width: 40px;\n- height: 40px;\n- border-radius: 50%;\n- font-size: 1em;\n- font-weight: bold;\n- display: flex;\n- justify-content: center;\n- align-items: center;\n- margin-right: 15px;\n+ width: 40px;\n+ height: 40px;\n+ border-radius: 50%;\n+ font-size: 1em;\n+ font-weight: bold;\n+ display: flex;\n+ justify-content: center;\n+ align-items: center;\n+ margin-right: 15px;\n }\n \n .chat-messages .message .message-content, .threads .thread .message-content {\n- flex: 1;\n+ flex: 1;\n }\n \n .chat-messages .message .message-content .author, .threads .thread .message-content .author {\n- font-weight: bold;\n- margin-bottom: 3px;\n+ font-weight: bold;\n+ margin-bottom: 3px;\n }\n \n * {\n- word-break: break-word;\n- overflow-wrap: break-word;\n- hyphens: auto;\n+ word-break: break-word;\n+ overflow-wrap: break-word;\n+ hyphens: auto;\n }\n \n .highlight pre {\n- white-space: pre-wrap;\n- word-break: break-word;\n- overflow-wrap: break-word;\n- hyphens: auto;\n+ white-space: pre-wrap;\n+ word-break: break-word;\n+ overflow-wrap: break-word;\n+ hyphens: auto;\n }\n \n .chat-messages .message .message-content .text, .threads .thread .message-content .text {\n- margin-bottom: 5px;\n- word-break: break-word;\n- overflow-wrap: break-word;\n- hyphens: auto;\n+ margin-bottom: 5px;\n+ word-break: break-word;\n+ overflow-wrap: break-word;\n+ hyphens: auto;\n }\n \n .message-content {\n- max-width: 100%;\n+ max-width: 100%;\n }\n \n .message-content {\n \n- img, video, iframe, div {\n- max-width: 90%;\n- border-radius: 20px;\n- }\n+ img, video, iframe, div {\n+ max-width: 90%;\n+ border-radius: 20px;\n+ }\n }\n \n .chat-messages .message .message-content .time, .threads .thread .message-content .time {\n- font-size: 0.8em;\n+ font-size: 0.8em;\n }\n \n .chat-input {\n- padding: 15px;\n- display: flex;\n- align-items: center;\n+ padding: 15px;\n+ display: flex;\n+ align-items: center;\n }\n \n input[type=\"text\"], .chat-input textarea {\n- flex: 1;\n- color: white;\n- border: none;\n- padding: 10px;\n- border-radius: 5px;\n- resize: none;\n+ flex: 1;\n+ color: white;\n+ border: none;\n+ padding: 10px;\n+ border-radius: 5px;\n+ resize: none;\n }\n \n .chat-input upload-button {\n- color: white;\n- border: none;\n- padding: 10px 15px;\n- margin-left: 10px;\n- border-radius: 5px;\n- cursor: pointer;\n- font-size: 1em;\n- transition: background-color 0.3s;\n+ color: white;\n+ border: none;\n+ padding: 10px 15px;\n+ margin-left: 10px;\n+ border-radius: 5px;\n+ cursor: pointer;\n+ font-size: 1em;\n+ transition: background-color 0.3s;\n }\n \n .chat-input button:hover {\n }\n \n @media (max-width: 768px) {\n- .sidebar {\n- display: none;\n- }\n+ .sidebar {\n+ display: none;\n+ }\n }\n \n .message {\n- .text {\n- max-width: 100%;\n- word-wrap: break-word;\n- overflow-wrap: break-word;\n- hyphens: auto;\n- }\n+ .text {\n+ max-width: 100%;\n+ word-wrap: break-word;\n+ overflow-wrap: break-word;\n+ hyphens: auto;\n+ }\n \n- .avatar {\n- opacity: 0;\n- }\n+ .avatar {\n+ opacity: 0;\n+ }\n \n- .author, .time {\n- display: none;\n- }\n+ .author, .time {\n+ display: none;\n+ }\n }\n \n .message.switch-user {\n- .text img, iframe, video {\n- max-width: 90%;\n- border-radius: 20px;\n- }\n+ .text img, iframe, video {\n+ max-width: 90%;\n+ border-radius: 20px;\n+ }\n \n- .avatar {\n- user-select: none;\n- opacity: 1;\n- }\n+ .avatar {\n+ user-select: none;\n+ opacity: 1;\n+ }\n \n- .author {\n- display: block;\n- }\n+ .author {\n+ display: block;\n+ }\n }\n \n .message:has(+ .message.switch-user), .message:last-child {\n- .time {\n- display: block;\n- }\n+ .time {\n+ display: block;\n+ }\n }\n \n ::-webkit-scrollbar {\n- display: none;\n+ display: none;\n }\n \n ::-webkit-scrollbar-track {\n }\n \n ::-webkit-scrollbar-thumb {\n- border-radius: 3px;\n+ border-radius: 3px;\n }\n \n ::-webkit-scrollbar-thumb:hover {\n }\n \n .chat-messages, .threads {\n- scrollbar-width: none;\n+ scrollbar-width: none;\n }\n \n a {\n- text-decoration: none\n+ text-decoration: none\n }\n \n .sidebar {\n- width: 250px;\n- padding: 20px;\n- overflow-y: auto;\n+ width: 250px;\n+ padding: 20px;\n+ overflow-y: auto;\n }\n \n .sidebar h2 {\n- font-size: 1.2em;\n- margin-bottom: 20px;\n+ font-size: 1.2em;\n+ margin-bottom: 20px;\n }\n \n .sidebar ul {\n- list-style: none;\n+ list-style: none;\n }\n \n .sidebar ul li {\n- margin-bottom: 15px;\n+ margin-bottom: 15px;\n }\n \n .sidebar ul li a {\n- text-decoration: none;\n- font-size: 1em;\n- transition: color 0.3s;\n+ text-decoration: none;\n+ font-size: 1em;\n+ transition: color 0.3s;\n }\n \n .sidebar ul li a:hover {\n }"} +{"repo": ".", "date": "2025-03-08", "line": "Merge main", "commit": "dd5a9a23e8e452a03f7080656608617309bf73a5", "diff": "commit dd5a9a23e8e452a03f7080656608617309bf73a5\nMerge: e91ae45 8e195a4\nAuthor: BordedDev \nDate: Sat Mar 8 18:40:43 2025 +0000\n\n Merge branch 'main' into main"} +{"repo": ".", "date": "2025-03-08", "line": "Merge: Tweaks for login/registration and base + image roundness", "commit": "aedfe9aa947dcd2262c825af5a4d977eb298ccb5", "diff": "commit aedfe9aa947dcd2262c825af5a4d977eb298ccb5\nMerge: 8e195a4 dd5a9a2\nAuthor: retoor \nDate: Sat Mar 8 18:53:02 2025 +0000\n\n \n Reviewed-by: retoor "} +{"repo": ".", "date": "2025-03-08", "line": "feat: Sort channels by last message time", "commit": "a219ce4d79a15ef900583ab025fb0da1df79ace3", "diff": "commit a219ce4d79a15ef900583ab025fb0da1df79ace3\nAuthor: retoor \nDate: Sat Mar 8 20:21:02 2025 +0100\n\n Update sorting.\n\ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex 3ae7902..be44df9 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -58,6 +58,9 @@ class WebView(BaseView):\n async for subscribed_channel in self.app.services.channel_member.find(user_uid=self.session.get(\"uid\"), deleted_at=None, is_banned=False):\n item = {}\n other_user = await self.app.services.channel_member.get_other_dm_user(subscribed_channel[\"channel_uid\"], self.session.get(\"uid\"))\n+ parent_object = await subscribed_channel.get_channel()\n+ last_message =await parent_object.get_last_message()\n+ item[\"last_message_on\"] = parent_object[\"last_message_on\"]\n if other_user:\n item[\"name\"] = other_user[\"nick\"]\n item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n@@ -65,6 +68,8 @@ class WebView(BaseView):\n item[\"name\"] = subscribed_channel[\"label\"]\n item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n channels.append(item)\n+ \n+ channels.sort(key=lambda x: x['last_message_on'], reverse=True)\n \n name = await channel_member.get_name()\n return await self.render_template(\"web.html\", {\"name\": name, \"channel\": channel,\"user\": user,\"messages\": messages , \"channels\": channels})"} +{"repo": ".", "date": "2025-03-08", "line": "feat: Separate public and private channels in sidebar", "commit": "d6061cb45b68a7b393b0e35a560d5d7bea4b9478", "diff": "commit d6061cb45b68a7b393b0e35a560d5d7bea4b9478\nAuthor: retoor \nDate: Sat Mar 8 20:27:05 2025 +0100\n\n Changes.\n\ndiff --git a/src/snek/templates/sidebar_channels.html b/src/snek/templates/sidebar_channels.html\nindex ed16ffd..9dd20b5 100644\n--- a/src/snek/templates/sidebar_channels.html\n+++ b/src/snek/templates/sidebar_channels.html\n@@ -6,7 +6,13 @@\n
\n-\n \n \n-\n {% block head %}\n {% endblock %}\n \n@@ -35,4 +35,4 @@\n {% endblock %}\n
\n \n-\n\\ No newline at end of file\n+\ndiff --git a/src/snek/templates/index.html b/src/snek/templates/index.html\nindex 1894bc4..ffbd5bc 100644\n--- a/src/snek/templates/index.html\n+++ b/src/snek/templates/index.html\n@@ -7,6 +7,7 @@\n \n \n \n \n \n
"} +{"repo": ".", "date": "2025-03-17", "line": "fix: Scroll to the end of the message container", "commit": "54416ee84f88064897a824ae2c3a9e0ef2c1ccaa", "diff": "commit 54416ee84f88064897a824ae2c3a9e0ef2c1ccaa\nAuthor: retoor \nDate: Mon Mar 17 09:15:49 2025 +0100\n\n Added block end.\n\ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex e1e443b..932a1fa 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -138,7 +138,7 @@\n });\n lastMessage = messagesContainer.querySelector(\".message:last-child\");\n if (doScrollDown) { \n- lastMessage?.scrollIntoView({ inline: \"nearest\" });\n+ lastMessage?.scrollIntoView({ block: \"end\", inline: \"nearest\" });\n }\n }"} +{"repo": ".", "date": "2025-03-17", "line": "feat: Refactor header layout and logo display", "commit": "39fa8fa0cd7a725edc1f293a7e8d0ea0d5d5bca6", "diff": "commit 39fa8fa0cd7a725edc1f293a7e8d0ea0d5d5bca6\nAuthor: retoor \nDate: Mon Mar 17 17:01:01 2025 +0100\n\n Layout update.\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex 943e819..0b475d1 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -39,16 +39,19 @@ main {\n \n header {\n- padding: 10px 20px;\n+ padding-top: 10px;\n+ padding-left: 20px;\n+ padding-right: 20px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n \n header .logo {\n- font-size: 1.5em;\n font-weight: bold;\n+ font-size: 1.2em;\n }\n \n header nav a {\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 31f5d7f..a8bb473 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -21,7 +21,9 @@\n \n \n
\n-
Snek
\n+
Snek
\n+ \n+
{% block header_text %}{% endblock %}
\n \n+\n
\n
\n {% block sidebar %}\ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex 932a1fa..8a68f64 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -1,10 +1,9 @@\n {% extends \"app.html\" %}\n \n+{% block header_text %}

{{ name }}

{% endblock %} \n+\n {% block main %}\n
\n-
\n-

{{ name }}

\n-
\n
\n {% for message in messages %}\n {% autoescape false %}"} +{"repo": ".", "date": "2025-03-17", "line": "feat: Add Promise.withResolvers polyfill and update templates", "commit": "7fa2817f773b47737236f4bb700023bcf8b483f1", "diff": "commit 7fa2817f773b47737236f4bb700023bcf8b483f1\nAuthor: BordedDev <>\nDate: Mon Mar 17 22:05:46 2025 +0100\n\n Added Promise.withResolvers pollyfill\n\ndiff --git a/src/snek/static/polyfills/Promise.withResolvers.js b/src/snek/static/polyfills/Promise.withResolvers.js\nnew file mode 100644\nindex 0000000..03f0185\n--- /dev/null\n+++ b/src/snek/static/polyfills/Promise.withResolvers.js\n@@ -0,0 +1,8 @@\n+Promise.withResolvers = Promise.withResolvers || function() {\n+ let resolve, reject;\n+ let promise = new Promise((res, rej) => {\n+ resolve = res;\n+ reject = rej;\n+ });\n+ return { promise, resolve, reject };\n+}\n\\ No newline at end of file\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 31f5d7f..2d96495 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -6,6 +6,7 @@\n \n Snek\n \n+ \n \ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex df0796c..d5df6eb 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -12,7 +12,8 @@\n \n {% block title %}Snek chat by Molodetz{% endblock %}\n \n- \n+ \n+ \n \n \n \n@@ -20,7 +21,8 @@\n \n \n \n+ data-website-id=\"d127c3e4-dc70-4041-a1c8-bcc32c2492ea\">\n {% block head %}\n {% endblock %}\n "} +{"repo": ".", "date": "2025-03-17", "line": "revert: Removed unnecessary whitespace", "commit": "965dc930a900a5080e225bb492be2b799daed22f", "diff": "commit 965dc930a900a5080e225bb492be2b799daed22f\nAuthor: BordedDev <>\nDate: Mon Mar 17 22:06:52 2025 +0100\n\n Undid formatting\n\ndiff --git a/src/snek/templates/base.html b/src/snek/templates/base.html\nindex d5df6eb..d2f8de7 100644\n--- a/src/snek/templates/base.html\n+++ b/src/snek/templates/base.html\n@@ -21,8 +21,7 @@\n \n \n \n- data-website-id=\"d127c3e4-dc70-4041-a1c8-bcc32c2492ea\">\n {% block head %}\n {% endblock %}\n "} +{"repo": ".", "date": "2025-03-17", "line": "feat: Add Promise.withResolvers polyfill", "commit": "825ece4e7868be28ba03c4fde5149695b0dd9dc5", "diff": "commit 825ece4e7868be28ba03c4fde5149695b0dd9dc5\nMerge: 39fa8fa 965dc93\nAuthor: retoor \nDate: Mon Mar 17 21:09:55 2025 +0000\n\n \n Reviewed-by: retoor "} +{"repo": ".", "date": "2025-03-18", "line": "feat: Added dump script for public channels", "commit": "3c6a0944d68ca16250ec9364d7f006b0e7eea6e8", "diff": "commit 3c6a0944d68ca16250ec9364d7f006b0e7eea6e8\nAuthor: retoor \nDate: Tue Mar 18 00:46:32 2025 +0100\n\n Added dump script.\n\ndiff --git a/Makefile b/Makefile\nindex e3febf7..70b06a0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -8,6 +8,9 @@ PORT = 8081\n python:\n \t$(PYTHON)\n \n+dump:\n+\t$(PYTHON) -m snek.dump\n+\n run:\n \t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\n \t\ndiff --git a/src/snek/dump.py b/src/snek/dump.py\nnew file mode 100644\nindex 0000000..6401637\n--- /dev/null\n+++ b/src/snek/dump.py\n@@ -0,0 +1,17 @@\n+import json \n+\n+from snek.app import app\n+\n+\n+def dump_public_channels():\n+ result = {'channels':{}}\n+\n+ for channel in app.db['channel'].find(is_private=False,is_listed=True):\n+ result['channels'][channel['label']] = dict(channel)\n+ result['channels'][channel['label']]['messages'] = list(dict(record) for record in app.db['channel_message'].find(channel_uid=channel['uid']))\n+\n+ print(json.dumps(result, sort_keys=True, indent=4,default=str),end='',flush=True)\n+ \n+\n+if __name__ == '__main__':\n+ dump_public_channels()"} +{"repo": ".", "date": "2025-03-18", "line": "feat: Improved dump functionality with JSON output and user information\n\n", "commit": "70db15bf27c7a8fd8bf112432f02754f01bbb3d7", "diff": "commit 70db15bf27c7a8fd8bf112432f02754f01bbb3d7\nAuthor: retoor \nDate: Tue Mar 18 00:13:25 2025 +0000\n\n Update.\n\ndiff --git a/Makefile b/Makefile\nindex 70b06a0..62d24ca 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -9,7 +9,7 @@ python:\n \t$(PYTHON)\n \n dump:\n-\t$(PYTHON) -m snek.dump\n+\t@$(PYTHON) -m snek.dump\n \n run:\n \t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\ndiff --git a/src/snek/dump.py b/src/snek/dump.py\nindex 6401637..cf3d417 100644\n--- a/src/snek/dump.py\n+++ b/src/snek/dump.py\n@@ -1,17 +1,33 @@\n+import asyncio\n import json \n \n from snek.app import app\n \n+async def fix_message(message):\n+ message = dict(\n+ uid=message['uid'],\n+ user_uid=message['user_uid'],\n+ text=message['message'],\n+ sent=message['created_at']\n+ )\n+ user = await app.services.user.get(uid=message['user_uid'])\n+ message['user'] = user and user['username'] or None\n+ return message\n \n-def dump_public_channels():\n+async def dump_public_channels():\n result = {'channels':{}}\n-\n- for channel in app.db['channel'].find(is_private=False,is_listed=True):\n+ for channel in app.db['channel'].find(is_private=False,is_listed=True,tag='public'):\n+ print(f\"Dumping channel: {channel['label']}.\")\n result['channels'][channel['label']] = dict(channel)\n- result['channels'][channel['label']]['messages'] = list(dict(record) for record in app.db['channel_message'].find(channel_uid=channel['uid']))\n-\n- print(json.dumps(result, sort_keys=True, indent=4,default=str),end='',flush=True)\n+ result['channels'][channel['label']]['messages'] = [await fix_message(record) for record in app.db['channel_message'].find(channel_uid=channel['uid'],order_by='created_at')]\n+ print(\"Dump succesfull!\")\n+ print(\"Converting to json.\")\n+ data = json.dumps(result, indent=4,default=str)\n+ print(\"Converting succesful, now writing to dump.json\")\n+ with open(\"dump.json\",\"w\") as f:\n+ f.write(data)\n+ print(\"Dump written to dump.json\")\n \n \n if __name__ == '__main__':\n- dump_public_channels()\n+ asyncio.run(dump_public_channels())\ndiff --git a/src/snek/system/cache.py b/src/snek/system/cache.py\nindex f9c4761..cd9484d 100644\n--- a/src/snek/system/cache.py\n+++ b/src/snek/system/cache.py\n@@ -20,13 +20,13 @@ class Cache:\n try:\n self.lru.pop(self.lru.index(args))\n except:\n- print(\"Cache miss!\", args, flush=True)\n return None\n self.lru.insert(0, args)\n while len(self.lru) > self.max_items:\n self.cache.pop(self.lru[-1])\n self.lru.pop()\n- print(\"Cache hit!\", args, flush=True)\n return self.cache[args]\n \n def json_default(self, value):\n@@ -61,7 +61,7 @@ class Cache:\n \n if is_new:\n self.version += 1\n- print(f\"Cache store! {len(self.lru)} items. New version:\", self.version, flush=True)\n \n async def delete(self, args):\n if args in self.cache:"} +{"repo": ".", "date": "2025-03-18", "line": "feat: Refactor dump script to output to text file", "commit": "3960390ec45f427979ebd2b81c1a21666a47e71d", "diff": "commit 3960390ec45f427979ebd2b81c1a21666a47e71d\nAuthor: retoor \nDate: Tue Mar 18 21:06:28 2025 +0000\n\n Updated dump script.\n\ndiff --git a/src/snek/dump.py b/src/snek/dump.py\nindex cf3d417..1b7eb6b 100644\n--- a/src/snek/dump.py\n+++ b/src/snek/dump.py\n@@ -12,20 +12,18 @@ async def fix_message(message):\n )\n user = await app.services.user.get(uid=message['user_uid'])\n message['user'] = user and user['username'] or None\n- return message\n+ return (message['user'] or '') + ': ' + (message['text'] or '')\n \n async def dump_public_channels():\n- result = {'channels':{}}\n+ result = []\n for channel in app.db['channel'].find(is_private=False,is_listed=True,tag='public'):\n print(f\"Dumping channel: {channel['label']}.\")\n- result['channels'][channel['label']] = dict(channel)\n- result['channels'][channel['label']]['messages'] = [await fix_message(record) for record in app.db['channel_message'].find(channel_uid=channel['uid'],order_by='created_at')]\n+ result += [await fix_message(record) for record in app.db['channel_message'].find(channel_uid=channel['uid'],order_by='created_at')]\n print(\"Dump succesfull!\")\n print(\"Converting to json.\")\n- data = json.dumps(result, indent=4,default=str)\n print(\"Converting succesful, now writing to dump.json\")\n- with open(\"dump.json\",\"w\") as f:\n- f.write(data)\n+ with open(\"dump.txt\",\"w\") as f:\n+ f.write('\\n\\n'.join(result))\n print(\"Dump written to dump.json\")"} +{"repo": ".", "date": "2025-03-20", "line": "refactor: Update session management for user registration", "commit": "5ba239caa8928a078362af0e6e2d1a4626bd508d", "diff": "commit 5ba239caa8928a078362af0e6e2d1a4626bd508d\nAuthor: retoor \nDate: Thu Mar 20 02:12:00 2025 +0100\n\n Changes by r.\n\ndiff --git a/src/snek/view/login.py b/src/snek/view/login.py\nindex 6d6d6ad..5028a7a 100644\n--- a/src/snek/view/login.py\n+++ b/src/snek/view/login.py\n@@ -31,4 +31,4 @@ class LoginView(BaseFormView):\n \"color\": user[\"color\"]\n })\n return {\"redirect_url\": \"/web.html\"}\n- return {\"is_valid\": False}\n+ return {\"is_valid\": False}\n\\ No newline at end of file\ndiff --git a/src/snek/view/register.py b/src/snek/view/register.py\nindex db812b5..6e49506 100644\n--- a/src/snek/view/register.py\n+++ b/src/snek/view/register.py\n@@ -24,8 +24,10 @@ class RegisterView(BaseFormView):\n result = await self.app.services.user.register(\n form.email.value, form.username.value, form.password.value\n )\n- self.request.session[\"uid\"] = result[\"uid\"]\n- self.request.session[\"username\"] = result[\"username\"]\n- self.request.session[\"logged_in\"] = True\n- self.request.session[\"color\"] = result[\"color\"]\n+ self.request.session.update({\n+ \"uid\": result[\"uid\"],\n+ \"username\": result[\"username\"],\n+ \"logged_in\": True,\n+ \"color\": result[\"color\"]\n+ })\n return {\"redirect_url\": \"/web.html\"}\n\\ No newline at end of file\ndiff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py\nindex 858edad..5ce42a7 100644\n--- a/src/snek/view/register_form.py\n+++ b/src/snek/view/register_form.py\n@@ -14,21 +14,20 @@\n \n from snek.form.register import RegisterForm\n from snek.system.view import BaseFormView\n \n-\n class RegisterFormView(BaseFormView):\n form = RegisterForm\n \n@@ -36,8 +35,10 @@ class RegisterFormView(BaseFormView):\n result = await self.app.services.user.register(\n form.email.value, form.username.value, form.password.value\n )\n- self.request.session[\"uid\"] = result[\"uid\"]\n- self.request.session[\"username\"] = result[\"username\"]\n- self.request.session[\"logged_in\"] = True\n-\n+ self.request.session.update({\n+ \"uid\": result[\"uid\"],\n+ \"username\": result[\"username\"],\n+ \"logged_in\": True,\n+ \"color\": result[\"color\"]\n+ })\n return {\"redirect_url\": \"/web.html\"}\n\\ No newline at end of file\ndiff --git a/src/snek/view/status.py b/src/snek/view/status.py\nindex 9428f08..117942a 100644\n--- a/src/snek/view/status.py\n+++ b/src/snek/view/status.py\n@@ -19,11 +19,10 @@\n \n-\n from snek.system.view import BaseView\n \n class StatusView(BaseView):"} +{"repo": ".", "date": "2025-03-22", "line": "feat: Added terminal access and updated Dockerfile and compose.yml", "commit": "7dcabde2ed0ab699ea3033f7788381e85c352b97", "diff": "commit 7dcabde2ed0ab699ea3033f7788381e85c352b97\nAuthor: retoor \nDate: Sat Mar 22 18:16:36 2025 +0100\n\n Update.\n\ndiff --git a/Dockerfile b/Dockerfile\nindex ffdc3d7..6ef9f83 100644\n--- a/Dockerfile\n+++ b/Dockerfile\n@@ -13,6 +13,7 @@ RUN apk add --no-cache \\\n libxext \\\n libssl3 \\\n ca-certificates \\\n+ docker \\\n fontconfig \\\n freetype \\\n ttf-dejavu \\\ndiff --git a/compose.yml b/compose.yml\nindex 17bcf22..70d21ba 100644\n--- a/compose.yml\n+++ b/compose.yml\n@@ -2,10 +2,14 @@ services:\n snek:\n build: .\n restart: always\n+ privileged: true\n ports:\n - \"8081:8081\"\n volumes:\n - ./:/code\n+ - /media/storage/snek/molodetz.nl/drive:/code/drive\n+ - /var/run/docker.sock:/var/run/docker.sock\n+ - /media/storage/snek/molodetz.nl/drive:/drive\n environment:\n - PYTHONDONTWRITEBYTECODE=1\n - PYTHONUNBUFFERED=1\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex e1f74e2..a7bc195 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -37,7 +37,7 @@ from snek.view.upload import UploadView\n from snek.view.search_user import SearchUserView \n from snek.view.avatar import AvatarView\n from snek.system.profiler import profiler_handler\n-\n+from snek.view.terminal import TerminalView, TerminalSocketView\n \n SESSION_KEY = b\"c79a0c5fda4b424189c427d28c9f7c34\"\n \n@@ -115,6 +115,8 @@ class Application(BaseApplication):\n self.router.add_get(\"/rpc.ws\", RPCView)\n self.router.add_view(\"/channel/{channel}.html\", WebView)\n self.router.add_view(\"/threads.html\", ThreadsView)\n+ self.router.add_view(\"/terminal.ws\", TerminalSocketView)\n+ self.router.add_view(\"/terminal.html\", TerminalView)\n \n self.add_subapp(\n \"/docs\","} +{"repo": ".", "date": "2025-03-22", "line": "feat: Added terminal access and basic shell environment.", "commit": "013d4adce57f4afec5176bbdb5e2225d529ec3b7", "diff": "commit 013d4adce57f4afec5176bbdb5e2225d529ec3b7\nAuthor: retoor \nDate: Sat Mar 22 19:57:39 2025 +0100\n\n Drive access.\n\ndiff --git a/Dockerfile b/Dockerfile\nindex 6ef9f83..d24c47d 100644\n--- a/Dockerfile\n+++ b/Dockerfile\n@@ -1,41 +1,10 @@\n-FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf \n-FROM python:3.12.8-alpine3.21\n+FROM python:3.14.0a6-bookworm\n+RUN mkdir -p /code \n WORKDIR /code\n-ENV FLASK_APP=app.py\n-ENV FLASK_RUN_HOST=0.0.0.0\n-RUN apk add --no-cache gcc musl-dev linux-headers git\n-\n-RUN apk add --no-cache \\\n- libstdc++ \\\n- libx11 \\\n- libxrender \\\n- libxext \\\n- libssl3 \\\n- ca-certificates \\\n- docker \\\n- fontconfig \\\n- freetype \\\n- ttf-dejavu \\\n- ttf-droid \\\n- ttf-freefont \\\n- ttf-liberation \\\n- && apk add --no-cache --virtual .build-deps \\\n- msttcorefonts-installer \\\n- && update-ms-fonts \\\n- && fc-cache -f \\\n- && apk del .build-deps\n-COPY --from=wkhtmltopdf /bin/wkhtmltopdf /bin/wkhtmltopdf\n-COPY --from=wkhtmltopdf /bin/wkhtmltoimage /bin/wkhtmltoimage\n+RUN apt update && apt install build-essential docker -y \n COPY pyproject.toml pyproject.toml \n COPY src src\n+RUN mkdir /drive\n RUN pip install --upgrade pip\n RUN pip install -e .\n-EXPOSE 8081\n \n-CMD [\"python\",\"-m\",\"snek.app\"]\ndiff --git a/compose.yml b/compose.yml\nindex 70d21ba..9d55d3b 100644\n--- a/compose.yml\n+++ b/compose.yml\n@@ -7,26 +7,12 @@ services:\n - \"8081:8081\"\n volumes:\n - ./:/code\n- - /media/storage/snek/molodetz.nl/drive:/code/drive\n+ - /media/storage/snek.molodetz.nl/drive:/code/drive\n+ - /media/storage/snek.molodetz.nl/drive:/drive\n - /var/run/docker.sock:/var/run/docker.sock\n- - /media/storage/snek/molodetz.nl/drive:/drive\n environment:\n - PYTHONDONTWRITEBYTECODE=1\n - PYTHONUNBUFFERED=1\n entrypoint: [\"gunicorn\", \"-w\", \"1\", \"-k\", \"aiohttp.worker.GunicornWebWorker\", \"snek.gunicorn:app\",\"--bind\",\"0.0.0.0:8081\"]\n- snecssh:\n- build:\n- context: .\n- dockerfile: DockerfileDrive\n- restart: always\n- ports:\n- - \"2225:2225\"\n- volumes:\n- - ./:/code\n- environment:\n- - PYTHONDONTWRITEBYTECODE=\"1\"\n- - PYTHONUNBUFFERED=\"1\"\n- entrypoint: [\"python\",\"-m\",\"snekssh.app2\"]\n \ndiff --git a/src/snek/scripts/chat.js b/src/snek/scripts/chat.js\nnew file mode 100644\nindex 0000000..a4d6782\n--- /dev/null\n+++ b/src/snek/scripts/chat.js\n@@ -0,0 +1,54 @@\n+const channelUid = \"{{ channel.uid.value }}\";\n+\n+function initInputField(textBox) {\n+ textBox.addEventListener('change', (e) => {\n+ e.preventDefault();\n+ this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));\n+ });\n+\n+ textBox.addEventListener('keydown', (e) => {\n+ if (e.key === 'Enter' && !e.shiftKey) {\n+ e.preventDefault();\n+ const message = e.target.value.trim();\n+ if (message) {\n+ app.rpc.sendMessage(channelUid, message);\n+ e.target.value = '';\n+ }\n+ }\n+ });\n+ textBox.focus();\n+}\n+\n+function updateTimes() {\n+ document.querySelectorAll(\".time\").forEach((time) => {\n+ time.innerText = app.timeDescription(time.dataset.created_at);\n+ });\n+}\n+\n+function isElementVisible(element) {\n+ const rect = element.getBoundingClientRect();\n+ return (\n+ rect.top >= 0 &&\n+ rect.left >= 0 &&\n+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n+ rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n+ );\n+}\n+\n+const messagesContainer = document.querySelector(\".chat-messages\");\n+\n+let isLoadingExtra = false;\n+\n+messagesContainer.addEventListener(\"scroll\", () => {\n+ loadExtra();\n+});\n+\n+setInterval(updateTimes, 1000);\n+\n+app.addEventListener(\"channel-message\", (data) => {\n+ if (data.channel_uid !== channelUid) {\n+ if(!isMentionForSomeoneElse(data.message)){\n+ channelSidebar.notify(data);\n+ }\n+ }\n+});\ndiff --git a/src/snek/system/terminal.py b/src/snek/system/terminal.py\nnew file mode 100644\nindex 0000000..2d9341e\n--- /dev/null\n+++ b/src/snek/system/terminal.py\n@@ -0,0 +1,48 @@\n+import asyncio\n+import aiohttp\n+import aiohttp.web\n+import os\n+import pty\n+import shlex\n+import subprocess\n+import pathlib\n+\n+commands = {\n+ 'alpine': 'docker run -it alpine /bin/sh',\n+ 'r': 'docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh',\n+}\n+\n+class TerminalSession:\n+ def __init__(self,command):\n+ self.master, self.slave = pty.openpty()\n+ self.sockets =[]\n+ self.process = subprocess.Popen(\n+ command.split(\" \"),\n+ stdin=self.slave,\n+ stdout=self.slave,\n+ stderr=self.slave,\n+ bufsize=0,\n+ universal_newlines=True\n+ )\n+\n+ async def read_output(self, ws):\n+ loop = asyncio.get_event_loop()\n+ self.sockets.append(ws)\n+ if len(self.sockets) > 1:\n+ return \n+ while True:\n+ try:\n+ data = await loop.run_in_executor(None, os.read, self.master, 1024)\n+ if not data:\n+ break\n+ try:\n+ except:\n+ self.sockets.remove(ws)\n+ except Exception:\n+ break\n+\n+ async def write_input(self, data):\n+ os.write(self.master, data.encode())\n+\n+\ndiff --git a/src/snek/templates/static b/src/snek/templates/static\nnew file mode 120000\nindex 0000000..d9bc54d\n--- /dev/null\n+++ b/src/snek/templates/static\n@@ -0,0 +1 @@\n+../static/\n\\ No newline at end of file\ndiff --git a/src/snek/templates/terminal.html b/src/snek/templates/terminal.html\nnew file mode 100644\nindex 0000000..d9ad6d1\n--- /dev/null\n+++ b/src/snek/templates/terminal.html\n@@ -0,0 +1,47 @@\n+{% extends \"app.html\" %}\n+\n+{% block sidebar %}\n+Reboot\n+{% endblock %}\n+\n+{% block main %}\n+ \n+\n+
\n+\n+ \n+\n+\n+\n+{% endblock main %}\ndiff --git a/src/snek/view/terminal.py b/src/snek/view/terminal.py\nnew file mode 100644\nindex 0000000..e54dfb7\n--- /dev/null\n+++ b/src/snek/view/terminal.py\n@@ -0,0 +1,56 @@\n+from snek.system.view import BaseView \n+import aiohttp \n+import asyncio\n+from snek.system.terminal import TerminalSession\n+import pathlib\n+\n+class TerminalSocketView(BaseView):\n+ \n+ login_required = True\n+\n+ user_sessions = {}\n+ \n+ async def prepare_drive(self):\n+ user = await self.services.user.get(uid=self.session.get(\"uid\"))\n+ root = pathlib.Path(\"drive\").joinpath(user[\"uid\"])\n+ root.mkdir(parents=True, exist_ok=True)\n+ terminal_folder = pathlib.Path(\"terminal\")\n+ for path in terminal_folder.iterdir():\n+ destination_path = root.joinpath(path.name)\n+ if not destination_path.exists():\n+ if not path.is_dir():\n+ destination_path.write_bytes(path.read_bytes())\n+ return root \n+ \n+ async def get(self):\n+ \n+\n+\n+ ws = aiohttp.web.WebSocketResponse()\n+ await ws.prepare(self.request)\n+ user = await self.services.user.get(uid=self.session.get(\"uid\"))\n+ root = await self.prepare_drive()\n+ \n+ command = f\"docker run -v ./{root}/:/root --rm -it --memory 512M --cpus=0.5 -w /root ubuntu:latest /bin/bash\"\n+ print(command)\n+\n+ session = self.user_sessions.get(user[\"uid\"])\n+ if not session:\n+ self.user_sessions[user[\"uid\"]] = TerminalSession(command=command)\n+ session = self.user_sessions[user[\"uid\"]] \n+ asyncio.create_task(session.read_output(ws)) \n+\n+ async for msg in ws:\n+ if msg.type == aiohttp.WSMsgType.BINARY:\n+ await session.write_input(msg.data.decode())\n+\n+ \n+ return ws\n+\n+class TerminalView(BaseView):\n+\n+ login_required = True\n+\n+ async def get(self):\n+ request = self.request\n+ return await self.request.app.render_template('terminal.html',self.request)\ndiff --git a/terminal/.bashrc b/terminal/.bashrc\nnew file mode 100644\nindex 0000000..2b5d791\n--- /dev/null\n+++ b/terminal/.bashrc\n@@ -0,0 +1,108 @@\n+\n+[ -z \"$PS1\" ] && return\n+\n+HISTCONTROL=ignoredups:ignorespace\n+\n+shopt -s histappend\n+\n+HISTSIZE=1000\n+HISTFILESIZE=2000\n+\n+shopt -s checkwinsize\n+\n+[ -x /usr/bin/lesspipe ] && eval \"$(SHELL=/bin/sh lesspipe)\"\n+\n+if [ -z \"$debian_chroot\" ] && [ -r /etc/debian_chroot ]; then\n+ debian_chroot=$(cat /etc/debian_chroot)\n+fi\n+\n+case \"$TERM\" in\n+ xterm-color) color_prompt=yes;;\n+esac\n+\n+\n+if [ -n \"$force_color_prompt\" ]; then\n+ if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then\n+\tcolor_prompt=yes\n+ else\n+\tcolor_prompt=\n+ fi\n+fi\n+\n+if [ \"$color_prompt\" = yes ]; then\n+ PS1='${debian_chroot:+($debian_chroot)}\\[\\033[01;32m\\]\\u@\\h\\[\\033[00m\\]:\\[\\033[01;34m\\]\\w\\[\\033[00m\\]\\$ '\n+else\n+ PS1='${debian_chroot:+($debian_chroot)}\\u@\\h:\\w\\$ '\n+fi\n+unset color_prompt force_color_prompt\n+\n+case \"$TERM\" in\n+xterm*|rxvt*)\n+ PS1=\"\\[\\e]0;${debian_chroot:+($debian_chroot)}\\u@\\h: \\w\\a\\]$PS1\"\n+ ;;\n+*)\n+ ;;\n+esac\n+\n+if [ -x /usr/bin/dircolors ]; then\n+ test -r ~/.dircolors && eval \"$(dircolors -b ~/.dircolors)\" || eval \"$(dircolors -b)\"\n+ alias ls='ls --color=auto'\n+\n+ alias grep='grep --color=auto'\n+ alias fgrep='fgrep --color=auto'\n+ alias egrep='egrep --color=auto'\n+fi\n+\n+alias ll='ls -alF'\n+alias la='ls -A'\n+alias l='ls -CF'\n+\n+\n+if [ -f ~/.bash_aliases ]; then\n+ . ~/.bash_aliases\n+fi\n+\n+\n+cp ~/r /usr/local/bin \n+\n+\n+apt update && apt install libreadline-dev libcurl4-openssl-dev libssl-dev libncurses5-dev libncursesw5-dev libsqlite3-dev libreadline6-dev zlib1g-dev libbz2-dev libffi-dev liblzma-dev python3 python3-pip python3-venv -y\n+\n+echo \"r is installed.\"\n+\ndiff --git a/terminal/.profile b/terminal/.profile\nnew file mode 100644\nindex 0000000..c4c7402\n--- /dev/null\n+++ b/terminal/.profile\n@@ -0,0 +1,9 @@\n+\n+if [ \"$BASH\" ]; then\n+ if [ -f ~/.bashrc ]; then\n+ . ~/.bashrc\n+ fi\n+fi\n+\n+mesg n 2> /dev/null || true\ndiff --git a/terminal/r b/terminal/r\nnew file mode 100755\nindex 0000000..2cc65df\nBinary files /dev/null and b/terminal/r differ"} +{"repo": ".", "date": "2025-03-22", "line": "feat: Added terminal page with Ubuntu option and channel sidebar", "commit": "6e68408ddfd8be2376f7453deac8f63a6bfb93e4", "diff": "commit 6e68408ddfd8be2376f7453deac8f63a6bfb93e4\nAuthor: retoor \nDate: Sat Mar 22 20:04:20 2025 +0100\n\n Update.\n\ndiff --git a/src/snek/templates/sidebar_channels.html b/src/snek/templates/sidebar_channels.html\nindex 9dd20b5..2d034b7 100644\n--- a/src/snek/templates/sidebar_channels.html\n+++ b/src/snek/templates/sidebar_channels.html\n@@ -4,6 +4,11 @@\n }\n \n \n \ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex 55accc7..b5c3504 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -90,13 +90,20 @@ class RPCView(BaseView):\n channels = []\n async for subscription in self.services.channel_member.find(user_uid=self.user_uid, is_banned=False):\n channel = await self.services.channel.get(uid=subscription['channel_uid'])\n+ last_message = await channel.get_last_message()\n+ color = None \n+ if last_message:\n+ last_message_user = await last_message.get_user()\n+ color = last_message_user['color']\n channels.append({\n \"name\": subscription[\"label\"],\n \"uid\": subscription[\"channel_uid\"],\n \"tag\": channel[\"tag\"],\n \"new_count\": subscription[\"new_count\"],\n \"is_moderator\": subscription[\"is_moderator\"],\n- \"is_read_only\": subscription[\"is_read_only\"]\n+ \"is_read_only\": subscription[\"is_read_only\"],\n+ 'new_count': subscription['new_count'],\n+ 'color': color \n })\n return channels\n \n@@ -156,9 +163,11 @@ class RPCView(BaseView):\n if result != \"noresponse\":\n await self._send_json({\"callId\": call_id, \"success\": success, \"data\": result})\n except Exception as ex:\n+ print(str(ex), flush=True)\n await self._send_json({\"callId\": call_id, \"success\": False, \"data\": str(ex)})\n \n async def _send_json(self, obj):\n+ print(obj, flush=True)\n await self.ws.send_str(json.dumps(obj, default=str))\n \n \ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex cdde6e3..b9271c3 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -46,6 +46,9 @@ class WebView(BaseView):\n channel_member = await self.app.services.channel_member.get(user_uid=self.session.get(\"uid\"), channel_uid=channel[\"uid\"])\n if not channel_member:\n return web.HTTPNotFound()\n+ \n+ channel_member['new_count'] = 0\n+ await self.app.services.channel_member.save(channel_member)\n \n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n messages = [await self.app.services.channel_message.to_extended_dict(message) for message in await self.app.services.channel_message.offset(\n@@ -54,23 +57,5 @@ class WebView(BaseView):\n for message in messages:\n await self.app.services.notification.mark_as_read(self.session.get(\"uid\"),message[\"uid\"])\n \n- channels = []\n- async for subscribed_channel in self.app.services.channel_member.find(user_uid=self.session.get(\"uid\"), deleted_at=None, is_banned=False):\n- item = {}\n- other_user = await self.app.services.channel_member.get_other_dm_user(subscribed_channel[\"channel_uid\"], self.session.get(\"uid\"))\n- parent_object = await subscribed_channel.get_channel()\n- last_message =await parent_object.get_last_message()\n- item[\"last_message_on\"] = parent_object[\"last_message_on\"]\n- item[\"is_private\"] = parent_object[\"tag\"] == \"dm\"\n- if other_user:\n- item[\"name\"] = other_user[\"nick\"]\n- item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n- else:\n- item[\"name\"] = subscribed_channel[\"label\"]\n- item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n- channels.append(item)\n- \n- channels.sort(key=lambda x: x['last_message_on'] or '', reverse=True)\n-\n name = await channel_member.get_name()\n- return await self.render_template(\"web.html\", {\"name\": name, \"channel\": channel,\"user\": user,\"messages\": messages , \"channels\": channels})\n+ return await self.render_template(\"web.html\", {\"name\": name, \"channel\": channel,\"user\": user,\"messages\": messages})"} +{"repo": ".", "date": "2025-03-27", "line": "fix: Prevent errors when user data is missing during notification updates", "commit": "877ef7970d5b7bb5f8fd0af35adad5d8d071b14d", "diff": "commit 877ef7970d5b7bb5f8fd0af35adad5d8d071b14d\nAuthor: retoor \nDate: Thu Mar 27 17:15:39 2025 +0100\n\n Bugfix.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex a5d4ebd..e6915bf 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -41,6 +41,8 @@ class NotificationService(BaseService):\n channel_member['new_count'] += 1\n \n usr = await self.services.user.get(uid=channel_member[\"user_uid\"])\n+ if not usr:\n+ continue\n print(\"INSERTED FOR \",usr[\"username\"],flush=True)\n await self.services.channel_member.save(channel_member)"} +{"repo": ".", "date": "2025-03-27", "line": "refactor: Remove debugging prints", "commit": "87c189b3fe897cc15ee6ac311e987bf9fe7811b9", "diff": "commit 87c189b3fe897cc15ee6ac311e987bf9fe7811b9\nAuthor: retoor \nDate: Thu Mar 27 17:28:54 2025 +0100\n\n Removed useless prints.\n\ndiff --git a/src/snek/service/channel_member.py b/src/snek/service/channel_member.py\nindex 42415d1..5c6c7ee 100644\n--- a/src/snek/service/channel_member.py\n+++ b/src/snek/service/channel_member.py\n@@ -5,6 +5,13 @@ class ChannelMemberService(BaseService):\n \n mapper_name = \"channel_member\"\n \n+ async def mark_as_read(self, channel_uid, user_uid):\n+ channel_member = await self.get(\n+ channel_uid=channel_uid,\n+ user_uid=user_uid\n+ )\n+ channel_member[\"new_count\"] = 0\n+ return await self.save(channel_member)\n \n async def create(\n self,\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex e6915bf..0b54a23 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -43,7 +43,6 @@ class NotificationService(BaseService):\n usr = await self.services.user.get(uid=channel_member[\"user_uid\"])\n if not usr:\n continue\n- print(\"INSERTED FOR \",usr[\"username\"],flush=True)\n await self.services.channel_member.save(channel_member)\n \n model = await self.new()\ndiff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py\nindex e7415e1..3b6c7c6 100644\n--- a/src/snek/system/mapper.py\n+++ b/src/snek/system/mapper.py\n@@ -49,7 +49,6 @@ class BaseMapper:\n if not model.record.get(\"uid\"):\n raise Exception(f\"Attempt to save without uid: {model.record}.\")\n model.updated_at.update()\n- print(model.record,flush=True)\n return self.table.upsert(model.record, [\"uid\"])\n \n async def find(self, **kwargs) -> typing.AsyncGenerator:\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nindex 5371d33..3d52892 100644\n--- a/src/snek/system/view.py\n+++ b/src/snek/system/view.py\n@@ -70,4 +70,3 @@ class BaseFormView(BaseView):\n return await self.json_response(result)\n \n async def submit(self, model=None):\n- print(\"Submit sucess\")\ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex 8a68f64..28ad4d2 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -184,6 +184,7 @@\n setTimeout(() => {\n updateLayout(doScrollDownBecauseLastMessageIsVisible)\n }, 1000);\n+ app.rpc.markAsRead(channelUid);\n });\n \n initInputField(getInputField());\ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex b5c3504..27a8dcf 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -39,9 +39,9 @@ class RPCView(BaseView):\n def is_logged_in(self):\n return self.view.session.get(\"logged_in\", False)\n \n- async def mark_as_read(self, message_uid):\n+ async def mark_as_read(self, channel_uid):\n self._require_login()\n- await self.services.notification.mark_as_read(self.user_uid, message_uid)\n+ await self.services.channel_member.mark_as_read(channel_uid, self.user_uid) \n return True\n \n async def login(self, username, password):\n@@ -167,7 +167,6 @@ class RPCView(BaseView):\n await self._send_json({\"callId\": call_id, \"success\": False, \"data\": str(ex)})\n \n async def _send_json(self, obj):\n- print(obj, flush=True)\n await self.ws.send_str(json.dumps(obj, default=str))\n \n \ndiff --git a/src/snek/view/terminal.py b/src/snek/view/terminal.py\nindex 26de464..fac680c 100644\n--- a/src/snek/view/terminal.py\n+++ b/src/snek/view/terminal.py\n@@ -31,7 +31,6 @@ class TerminalSocketView(BaseView):\n root = await self.prepare_drive()\n \n command = f\"docker run -v ./{root}/:/root --rm -it --memory 512M --cpus=0.5 -w /root ubuntu:latest /bin/bash\"\n- print(command)\n \n session = self.user_sessions.get(user[\"uid\"])\n if not session:"} +{"repo": ".", "date": "2025-03-27", "line": "refactor: Removed unnecessary print statements", "commit": "71a206a19fa6b2a2ae05d886a39d2fa03f2c0f71", "diff": "commit 71a206a19fa6b2a2ae05d886a39d2fa03f2c0f71\nAuthor: retoor \nDate: Thu Mar 27 17:30:06 2025 +0100\n\n Removed useless prints.\n\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nindex 3d52892..2765642 100644\n--- a/src/snek/system/view.py\n+++ b/src/snek/system/view.py\n@@ -70,3 +70,4 @@ class BaseFormView(BaseView):\n return await self.json_response(result)\n \n async def submit(self, model=None):\n+ pass"} +{"repo": ".", "date": "2025-03-27", "line": "feat: Added database transaction for notification creation", "commit": "bd5bb5ae65d6cb870d090382b106b705833d4cf1", "diff": "commit bd5bb5ae65d6cb870d090382b106b705833d4cf1\nAuthor: retoor \nDate: Thu Mar 27 20:10:05 2025 +0100\n\n Transaction.\n\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 0b54a23..1392044 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -30,6 +30,7 @@ class NotificationService(BaseService):\n uid=channel_message_uid\n )\n user = await self.services.user.get(uid=channel_message[\"user_uid\"])\n+ self.app.db.begin()\n async for channel_member in self.services.channel_member.find(\n channel_uid=channel_message[\"channel_uid\"],\n is_banned=False,\n@@ -56,3 +57,5 @@ class NotificationService(BaseService):\n await self.save(model)\n except Exception as ex:\n raise Exception(f\"Failed to create notification: {model.errors}.\")\n+\n+ self.app.db.commit()"} +{"repo": ".", "date": "2025-03-27", "line": "feat: Persist RPC transaction changes to database", "commit": "13ce09a5c50ddba8956dbeb838f9dd1bcafe184a", "diff": "commit 13ce09a5c50ddba8956dbeb838f9dd1bcafe184a\nAuthor: retoor \nDate: Thu Mar 27 20:20:48 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex 27a8dcf..5720f04 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -203,7 +203,9 @@ class RPCView(BaseView):\n if msg.type == web.WSMsgType.TEXT:\n try:\n async with Profiler():\n+ self.app.db.begin()\n await rpc(msg.json())\n+ self.app.db.commit()\n except Exception as ex:\n print(ex, flush=True)\n await self.services.socket.delete(ws)"} +{"repo": ".", "date": "2025-03-27", "line": "feat: Added task runner and task queue for asynchronous operations", "commit": "145373399dada2f4cee54af0c99e62d4a27a0f99", "diff": "commit 145373399dada2f4cee54af0c99e62d4a27a0f99\nAuthor: retoor \nDate: Thu Mar 27 20:41:04 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 1d225cc..3094dd5 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -61,6 +61,7 @@ class Application(BaseApplication):\n middlewares=middlewares, template_path=self.template_path, *args, **kwargs\n )\n session_setup(self, EncryptedCookieStorage(SESSION_KEY))\n+ self.tasks = []\n self._middlewares.append(session_middleware)\n self.jinja2_env.add_extension(MarkdownExtension)\n self.jinja2_env.add_extension(LinkifyExtension)\n@@ -68,12 +69,28 @@ class Application(BaseApplication):\n self.jinja2_env.add_extension(EmojiExtension)\n \n self.setup_router()\n- \n+ \n self.cache = Cache(self)\n self.services = get_services(app=self)\n self.mappers = get_mappers(app=self)\n self.on_startup.append(self.prepare_database)\n \n+ async def create_task(self, task):\n+ self.tasks.append(task)\n+\n+ async def task_runner(self):\n+ while True:\n+ await asyncio.sleep(0.1)\n+ task = None\n+ try:\n+ task = self.tasks.pop(0)\n+ except IndexError:\n+ continue \n+ try:\n+ await task\n+ except:\n+ print(ex)\n+\n async def prepare_database(self,app):\n self.db.query(\"PRAGMA journal_mode=WAL\")\n self.db.query(\"PRAGMA syncnorm=off\")\n@@ -89,6 +106,7 @@ class Application(BaseApplication):\n pass \n \n await app.services.drive.prepare_all()\n+ self.loop.create_task(self.task_runner())\n \n def setup_router(self):\n self.router.add_get(\"/\", IndexView)\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex 17c677b..ecaedb7 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -21,7 +21,10 @@ class ChatService(BaseService):\n \n user = await self.services.user.get(uid=user_uid)\n await self.services.notification.create_channel_message(channel_message_uid)\n- sent_to_count = await self.services.socket.broadcast(channel_uid, dict(\n+ channel['last_message_on'] = now()\n+ await self.services.channel.save(channel)\n+ \n+ await self.app.create_task(self.services.socket.broadcast(channel_uid, dict(\n message=channel_message[\"message\"],\n html=channel_message[\"html\"],\n user_uid=user_uid,\n@@ -32,7 +35,5 @@ class ChatService(BaseService):\n username=user['username'],\n uid=channel_message['uid'],\n user_nick=user['nick']\n- ))\n- channel['last_message_on'] = now()\n- await self.services.channel.save(channel)\n- return sent_to_count\n\\ No newline at end of file\n+ )))\n+ return True"} +{"repo": ".", "date": "2025-03-27", "line": "refactor: Improve task management with asyncio.Queue and error handling", "commit": "e6f702a6b405f1dae0ce719177c6f7bf5b636ad8", "diff": "commit e6f702a6b405f1dae0ce719177c6f7bf5b636ad8\nAuthor: retoor \nDate: Thu Mar 27 20:56:35 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 3094dd5..7c0f0b0 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -61,7 +61,7 @@ class Application(BaseApplication):\n middlewares=middlewares, template_path=self.template_path, *args, **kwargs\n )\n session_setup(self, EncryptedCookieStorage(SESSION_KEY))\n- self.tasks = []\n+ self.tasks = asyncio.Queue()\n self._middlewares.append(session_middleware)\n self.jinja2_env.add_extension(MarkdownExtension)\n self.jinja2_env.add_extension(LinkifyExtension)\n@@ -76,19 +76,14 @@ class Application(BaseApplication):\n self.on_startup.append(self.prepare_database)\n \n async def create_task(self, task):\n- self.tasks.append(task)\n+ await self.tasks.put(task)\n \n async def task_runner(self):\n while True:\n- await asyncio.sleep(0.1)\n- task = None\n- try:\n- task = self.tasks.pop(0)\n- except IndexError:\n- continue \n+ task = await self.tasks.get() \n try:\n await task\n- except:\n+ except Exception as ex:\n print(ex)\n \n async def prepare_database(self,app):\ndiff --git a/src/snek/service/socket.py b/src/snek/service/socket.py\nindex 0b6071d..d941321 100644\n--- a/src/snek/service/socket.py\n+++ b/src/snek/service/socket.py\n@@ -62,12 +62,12 @@ class SocketService(BaseService):\n async def broadcast(self, channel_uid, message):\n count = 0\n async for channel_member in self.app.services.channel_member.find(channel_uid=channel_uid):\n- count += await self.send_to_user(channel_member[\"user_uid\"],message)\n- return count\n+ await self.send_to_user(channel_member[\"user_uid\"],message)\n+ return True \n \n async def delete(self, ws):\n for s in [sock for sock in self.sockets if sock.ws == ws]:\n await s.close()\n self.sockets.remove(s)\n \n- \n\\ No newline at end of file\n+"} +{"repo": ".", "date": "2025-03-27", "line": "fix: Remove unnecessary database transaction block in RPCView", "commit": "9d5815ed1028354ac61f2d506530147a02c476e6", "diff": "commit 9d5815ed1028354ac61f2d506530147a02c476e6\nAuthor: retoor \nDate: Thu Mar 27 21:00:34 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex 5720f04..27a8dcf 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -203,9 +203,7 @@ class RPCView(BaseView):\n if msg.type == web.WSMsgType.TEXT:\n try:\n async with Profiler():\n- self.app.db.begin()\n await rpc(msg.json())\n- self.app.db.commit()\n except Exception as ex:\n print(ex, flush=True)\n await self.services.socket.delete(ws)"} +{"repo": ".", "date": "2025-03-27", "line": "feat: Added database transaction management for task execution", "commit": "8810679fd8ea2dd6e63b09bf9a4b9af5c2d0fdd2", "diff": "commit 8810679fd8ea2dd6e63b09bf9a4b9af5c2d0fdd2\nAuthor: retoor \nDate: Thu Mar 27 21:01:17 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 7c0f0b0..67beb5d 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -81,10 +81,12 @@ class Application(BaseApplication):\n async def task_runner(self):\n while True:\n task = await self.tasks.get() \n+ self.db.begin()\n try:\n await task\n except Exception as ex:\n print(ex)\n+ self.db.commit()\n \n async def prepare_database(self,app):\n self.db.query(\"PRAGMA journal_mode=WAL\")"} +{"repo": ".", "date": "2025-03-27", "line": "feat: Added task execution time measurement", "commit": "6ad3844f037735e268b42247fcc6e8605cc13f07", "diff": "commit 6ad3844f037735e268b42247fcc6e8605cc13f07\nAuthor: retoor \nDate: Thu Mar 27 21:07:04 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 67beb5d..dd3e0f3 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -2,6 +2,7 @@ import pathlib\n import asyncio\n \n import logging\n+import time\n \n from snek.view.threads import ThreadsView \n \n@@ -83,7 +84,11 @@ class Application(BaseApplication):\n task = await self.tasks.get() \n self.db.begin()\n try:\n+ task_start = time.time()\n await task\n+ task_end = time.time()\n+ print(f\"Task {task} took {task_end - task_start} seconds\")\n+ self.tasks.task_done()\n except Exception as ex:\n print(ex)\n self.db.commit()"} +{"repo": ".", "date": "2025-03-27", "line": "feat: Asynchronously create channel messages and improve socket broadcast", "commit": "73e8779bdc42ec5f5618fad3d563544d1fad2b69", "diff": "commit 73e8779bdc42ec5f5618fad3d563544d1fad2b69\nAuthor: retoor \nDate: Thu Mar 27 21:11:02 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex ecaedb7..a008c70 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -20,11 +20,11 @@ class ChatService(BaseService):\n \n \n user = await self.services.user.get(uid=user_uid)\n- await self.services.notification.create_channel_message(channel_message_uid)\n+ async self.app.create_task(self.services.notification.create_channel_message(channel_message_uid))\n channel['last_message_on'] = now()\n await self.services.channel.save(channel)\n \n- await self.app.create_task(self.services.socket.broadcast(channel_uid, dict(\n+ self.services.socket.broadcast(channel_uid, dict(\n message=channel_message[\"message\"],\n html=channel_message[\"html\"],\n user_uid=user_uid,\n@@ -35,5 +35,5 @@ class ChatService(BaseService):\n username=user['username'],\n uid=channel_message['uid'],\n user_nick=user['nick']\n- )))\n+ ))\n return True"} +{"repo": ".", "date": "2025-03-27", "line": "fix: Ensure notification creation after socket broadcast", "commit": "6f043d21390d13d37772c8ae145d7a66b3919529", "diff": "commit 6f043d21390d13d37772c8ae145d7a66b3919529\nAuthor: retoor \nDate: Thu Mar 27 21:12:19 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex a008c70..aef0cdd 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -20,11 +20,10 @@ class ChatService(BaseService):\n \n \n user = await self.services.user.get(uid=user_uid)\n- async self.app.create_task(self.services.notification.create_channel_message(channel_message_uid))\n channel['last_message_on'] = now()\n await self.services.channel.save(channel)\n- \n- self.services.socket.broadcast(channel_uid, dict(\n+ \n+ await self.services.socket.broadcast(channel_uid, dict(\n message=channel_message[\"message\"],\n html=channel_message[\"html\"],\n user_uid=user_uid,\n@@ -36,4 +35,6 @@ class ChatService(BaseService):\n uid=channel_message['uid'],\n user_nick=user['nick']\n ))\n+ async self.app.create_task(self.services.notification.create_channel_message(channel_message_uid))\n+ \n return True"} +{"repo": ".", "date": "2025-03-27", "line": "fix: Await task creation in chat service", "commit": "9e50b2a6d3570c4f86fcfca6a5ce59947c0105ec", "diff": "commit 9e50b2a6d3570c4f86fcfca6a5ce59947c0105ec\nAuthor: retoor \nDate: Thu Mar 27 21:14:01 2025 +0100\n\n Transactions.\n\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex aef0cdd..8b1f8ad 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -35,6 +35,6 @@ class ChatService(BaseService):\n uid=channel_message['uid'],\n user_nick=user['nick']\n ))\n- async self.app.create_task(self.services.notification.create_channel_message(channel_message_uid))\n+ await self.app.create_task(self.services.notification.create_channel_message(channel_message_uid))\n \n return True"} +{"repo": ".", "date": "2025-03-28", "line": "feat: Initialized Ubuntu Dockerfile and Makefile build process.", "commit": "5fbcadad8bad7b8dd7ac3279938ff50db6b5d380", "diff": "commit 5fbcadad8bad7b8dd7ac3279938ff50db6b5d380\nAuthor: retoor \nDate: Fri Mar 28 02:41:57 2025 +0100\n\n Terminal Update.\n\ndiff --git a/DockerfileUbuntu b/DockerfileUbuntu\nnew file mode 100644\nindex 0000000..45c6038\n--- /dev/null\n+++ b/DockerfileUbuntu\n@@ -0,0 +1,11 @@\n+FROM ubuntu:latest\n+\n+RUN apt update && apt install libreadline-dev libcurl4-openssl-dev libssl-dev libncurses5-dev libncursesw5-dev libsqlite3-dev libreadline6-dev zlib1g-dev libbz2-dev libffi-dev liblzma-dev python3 python3-pip python3-venv libjson-c-dev vim htop git curl wget -y\n+\n+\n+RUN chmod +x r\n+\n+RUN cp r /usr/local/bin\n+\n+CMD [\"r\"]\ndiff --git a/Makefile b/Makefile\nindex c9a7a28..878e699 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -11,12 +11,15 @@ python:\n dump:\n \t@$(PYTHON) -m snek.dump\n \n+build:\n+\n+\n run:\n \t$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload\n \t\n install:\n \tpython3.12 -m venv .venv \n \t$(PIP) install -e .\n-\n+\tdocker build -f DockerfileUbuntu -t snek_ubuntu .\n \n \ndiff --git a/src/snek/system/terminal.py b/src/snek/system/terminal.py\nindex 6a91040..80dda17 100644\n--- a/src/snek/system/terminal.py\n+++ b/src/snek/system/terminal.py\n@@ -32,7 +32,7 @@ class TerminalSession:\n \n async def read_output(self, ws):\n self.sockets.append(ws)\n- if len(self.sockets) > 1 and self.buffer:\n+ if len(self.sockets) > 1 and self.history:\n start = 0\n try:\n start = self.history.index(b'\\n')\ndiff --git a/src/snek/view/terminal.py b/src/snek/view/terminal.py\nindex fac680c..6596f2c 100644\n--- a/src/snek/view/terminal.py\n+++ b/src/snek/view/terminal.py\n@@ -30,7 +30,10 @@ class TerminalSocketView(BaseView):\n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n root = await self.prepare_drive()\n \n- command = f\"docker run -v ./{root}/:/root --rm -it --memory 512M --cpus=0.5 -w /root ubuntu:latest /bin/bash\"\n+\n+ \n+\n+ command = f\"docker run -v ./{root}/:/root -it --memory 512M --cpus=0.5 -w /root snek_ubuntu /bin/bash\"\n \n session = self.user_sessions.get(user[\"uid\"])\n if not session:\ndiff --git a/terminal/.bashrc b/terminal/.bashrc\nindex e4022a9..ee7d93f 100644\n--- a/terminal/.bashrc\n+++ b/terminal/.bashrc\n@@ -92,11 +92,6 @@ if [ -f ~/.bash_aliases ]; then\n fi\n \n \n-cp ~/r /usr/local/bin \n-\n-chmod +x /usr/local/bin/r\n-\n-apt update && apt install libreadline-dev libcurl4-openssl-dev libssl-dev libncurses5-dev libncursesw5-dev libsqlite3-dev libreadline6-dev zlib1g-dev libbz2-dev libffi-dev liblzma-dev python3 python3-pip python3-venv libjson-c-dev vim htop git -y\n \n echo \"R is installed. Type r to run it.\"\n \ndiff --git a/terminal/r b/terminal/r\ndeleted file mode 100755\nindex 2cc65df..0000000\nBinary files a/terminal/r and /dev/null differ"} +{"repo": ".", "date": "2025-03-29", "line": "fix: Adjusted avatar size in message template", "commit": "fe1b3d6d191176111538f1ba06018e14c44ca8f9", "diff": "commit fe1b3d6d191176111538f1ba06018e14c44ca8f9\nAuthor: retoor \nDate: Sat Mar 29 01:21:33 2025 +0100\n\n Fixed avatar issue\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex 0b475d1..31a6e74 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -254,6 +254,7 @@ input[type=\"text\"], .chat-input textarea {\n }\n \n .avatar {\n+ \n opacity: 0;\n }\n \ndiff --git a/src/snek/templates/message.html b/src/snek/templates/message.html\nindex e8afc31..9773ae1 100644\n--- a/src/snek/templates/message.html\n+++ b/src/snek/templates/message.html\n@@ -1 +1 @@\n-
{{user_nick}}
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}
\n+
{{user_nick}}
{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}
"} +{"repo": ".", "date": "2025-03-29", "line": "```\nfeat: Added webdav support\n\nThis commit introduces webdav functionality to the application.\n- Added webdav dependency\n- Implemented WebdavApplication class\n- Added webdav route to the application\n- Added drive item mapper\n- Added drive mapper\n```", "commit": "29139d5d0c18ad2b0ebd32db9a0b629c45c0a651", "diff": "commit 29139d5d0c18ad2b0ebd32db9a0b629c45c0a651\nAuthor: retoor \nDate: Sat Mar 29 07:13:23 2025 +0100\n\n Added webdav.\n\ndiff --git a/pyproject.toml b/pyproject.toml\nindex e1075c9..6fbf200 100644\n--- a/pyproject.toml\n+++ b/pyproject.toml\n@@ -15,6 +15,8 @@ keywords = [\"chat\", \"snek\", \"molodetz\"]\n requires-python = \">=3.12\"\n dependencies = [\n \"mkdocs>=1.4.0\",\n+ \"lxml\",\n+\n \"shed\",\n \"beautifulsoup4\",\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex dd3e0f3..984fcf3 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -1,13 +1,14 @@\n-import pathlib\n import asyncio\n-\n import logging\n+import pathlib\n import time\n \n-from snek.view.threads import ThreadsView \n+from snek.view.threads import ThreadsView\n \n logging.basicConfig(level=logging.DEBUG)\n \n+from concurrent.futures import ThreadPoolExecutor\n+\n from aiohttp import web\n from aiohttp_session import (\n get_session as session_get,\n@@ -24,23 +25,24 @@ from snek.system import http\n from snek.system.cache import Cache\n from snek.system.markdown import MarkdownExtension\n from snek.system.middleware import cors_middleware\n-from snek.system.template import LinkifyExtension, PythonExtension,EmojiExtension\n+from snek.system.profiler import profiler_handler\n+from snek.system.template import EmojiExtension, LinkifyExtension, PythonExtension\n from snek.view.about import AboutHTMLView, AboutMDView\n+from snek.view.avatar import AvatarView\n from snek.view.docs import DocsHTMLView, DocsMDView\n+from snek.view.drive import DriveView\n from snek.view.index import IndexView\n from snek.view.login import LoginView\n from snek.view.logout import LogoutView\n from snek.view.register import RegisterView\n from snek.view.rpc import RPCView\n+from snek.view.search_user import SearchUserView\n from snek.view.status import StatusView\n-from snek.view.web import WebView\n+from snek.view.terminal import TerminalSocketView, TerminalView\n from snek.view.upload import UploadView\n-from snek.view.search_user import SearchUserView \n-from snek.view.avatar import AvatarView\n-from snek.system.profiler import profiler_handler\n-from snek.view.terminal import TerminalView, TerminalSocketView\n-from snek.view.drive import DriveView\n-from concurrent.futures import ThreadPoolExecutor\n+from snek.view.web import WebView\n+from snek.webdav import WebdavApplication\n+\n SESSION_KEY = b\"c79a0c5fda4b424189c427d28c9f7c34\"\n \n \n@@ -50,6 +52,15 @@ async def session_middleware(request, handler):\n response = await handler(request)\n return response\n \n+\n+@web.middleware\n+async def trailing_slash_middleware(request, handler):\n+ if request.path and not request.path.endswith(\"/\"):\n+ raise web.HTTPFound(request.path + \"/\")\n+ return await handler(request)\n+\n+\n class Application(BaseApplication):\n \n def __init__(self, *args, **kwargs):\n@@ -68,9 +79,9 @@ class Application(BaseApplication):\n self.jinja2_env.add_extension(LinkifyExtension)\n self.jinja2_env.add_extension(PythonExtension)\n self.jinja2_env.add_extension(EmojiExtension)\n- \n+\n self.setup_router()\n- \n+\n self.cache = Cache(self)\n self.services = get_services(app=self)\n self.mappers = get_mappers(app=self)\n@@ -81,7 +92,7 @@ class Application(BaseApplication):\n \n async def task_runner(self):\n while True:\n- task = await self.tasks.get() \n+ task = await self.tasks.get()\n self.db.begin()\n try:\n task_start = time.time()\n@@ -93,20 +104,20 @@ class Application(BaseApplication):\n print(ex)\n self.db.commit()\n \n- async def prepare_database(self,app):\n+ async def prepare_database(self, app):\n self.db.query(\"PRAGMA journal_mode=WAL\")\n self.db.query(\"PRAGMA syncnorm=off\")\n \n try:\n if not self.db[\"user\"].has_index(\"username\"):\n self.db[\"user\"].create_index(\"username\", unique=True)\n- if not self.db[\"channel_member\"].has_index([\"channel_uid\",\"user_uid\"]):\n- self.db[\"channel_member\"].create_index([\"channel_uid\",\"user_uid\"])\n- if not self.db[\"channel_message\"].has_index([\"channel_uid\",\"user_uid\"]):\n- self.db[\"channel_message\"].create_index([\"channel_uid\",\"user_uid\"])\n+ if not self.db[\"channel_member\"].has_index([\"channel_uid\", \"user_uid\"]):\n+ self.db[\"channel_member\"].create_index([\"channel_uid\", \"user_uid\"])\n+ if not self.db[\"channel_message\"].has_index([\"channel_uid\", \"user_uid\"]):\n+ self.db[\"channel_message\"].create_index([\"channel_uid\", \"user_uid\"])\n except:\n- pass \n- \n+ pass\n+\n await app.services.drive.prepare_all()\n self.loop.create_task(self.task_runner())\n \n@@ -145,6 +156,8 @@ class Application(BaseApplication):\n self.router.add_view(\"/terminal.html\", TerminalView)\n self.router.add_view(\"/drive.json\", DriveView)\n self.router.add_view(\"/drive/{drive}.json\", DriveView)\n+ self.webdav = WebdavApplication(self)\n+ self.add_subapp(\"/webdav\", self.webdav)\n \n self.add_subapp(\n \"/docs\",\n@@ -174,17 +187,21 @@ class Application(BaseApplication):\n channels = []\n if not context:\n context = {}\n- if request.session.get(\"uid\"): \n- async for subscribed_channel in self.services.channel_member.find(user_uid=request.session.get(\"uid\"), deleted_at=None, is_banned=False):\n+ if request.session.get(\"uid\"):\n+ async for subscribed_channel in self.services.channel_member.find(\n+ user_uid=request.session.get(\"uid\"), deleted_at=None, is_banned=False\n+ ):\n item = {}\n- other_user = await self.services.channel_member.get_other_dm_user(subscribed_channel[\"channel_uid\"], request.session.get(\"uid\"))\n+ other_user = await self.services.channel_member.get_other_dm_user(\n+ subscribed_channel[\"channel_uid\"], request.session.get(\"uid\")\n+ )\n parent_object = await subscribed_channel.get_channel()\n- last_message =await parent_object.get_last_message()\n- color = None \n+ last_message = await parent_object.get_last_message()\n+ color = None\n if last_message:\n last_message_user = await last_message.get_user()\n color = last_message_user[\"color\"]\n- item['color'] = color\n+ item[\"color\"] = color\n item[\"last_message_on\"] = parent_object[\"last_message_on\"]\n item[\"is_private\"] = parent_object[\"tag\"] == \"dm\"\n if other_user:\n@@ -193,19 +210,22 @@ class Application(BaseApplication):\n else:\n item[\"name\"] = subscribed_channel[\"label\"]\n item[\"uid\"] = subscribed_channel[\"channel_uid\"]\n- item['new_count'] = subscribed_channel['new_count'] \n- \n+ item[\"new_count\"] = subscribed_channel[\"new_count\"]\n+\n print(item)\n channels.append(item)\n- \n- channels.sort(key=lambda x: x['last_message_on'] or '', reverse=True)\n- if not 'channels' in context:\n- context['channels'] = channels\n- if not 'user' in context:\n- context['user'] = await self.services.user.get(uid=request.session.get(\"uid\"))\n+\n+ channels.sort(key=lambda x: x[\"last_message_on\"] or \"\", reverse=True)\n+ if \"channels\" not in context:\n+ context[\"channels\"] = channels\n+ if \"user\" not in context:\n+ context[\"user\"] = await self.services.user.get(\n+ uid=request.session.get(\"uid\")\n+ )\n \n return await super().render_template(template, request, context)\n \n+\n executor = ThreadPoolExecutor(max_workers=200)\n \n loop = asyncio.get_event_loop()\n@@ -213,8 +233,10 @@ loop.set_default_executor(executor)\n \n \n+\n async def main():\n await web._run_app(app, port=8081, host=\"0.0.0.0\")\n \n+\n if __name__ == \"__main__\":\n asyncio.run(main())\ndiff --git a/src/snek/dump.py b/src/snek/dump.py\nindex 1b7eb6b..b254756 100644\n--- a/src/snek/dump.py\n+++ b/src/snek/dump.py\n@@ -1,31 +1,39 @@\n import asyncio\n-import json \n \n from snek.app import app\n \n+\n async def fix_message(message):\n- message = dict(\n- uid=message['uid'],\n- user_uid=message['user_uid'],\n- text=message['message'],\n- sent=message['created_at']\n- )\n- user = await app.services.user.get(uid=message['user_uid'])\n- message['user'] = user and user['username'] or None\n- return (message['user'] or '') + ': ' + (message['text'] or '')\n+ message = {\n+ \"uid\": message[\"uid\"],\n+ \"user_uid\": message[\"user_uid\"],\n+ \"text\": message[\"message\"],\n+ \"sent\": message[\"created_at\"],\n+ }\n+ user = await app.services.user.get(uid=message[\"user_uid\"])\n+ message[\"user\"] = user and user[\"username\"] or None\n+ return (message[\"user\"] or \"\") + \": \" + (message[\"text\"] or \"\")\n+\n \n async def dump_public_channels():\n result = []\n- for channel in app.db['channel'].find(is_private=False,is_listed=True,tag='public'):\n+ for channel in app.db[\"channel\"].find(\n+ is_private=False, is_listed=True, tag=\"public\"\n+ ):\n print(f\"Dumping channel: {channel['label']}.\")\n- result += [await fix_message(record) for record in app.db['channel_message'].find(channel_uid=channel['uid'],order_by='created_at')]\n+ result += [\n+ await fix_message(record)\n+ for record in app.db[\"channel_message\"].find(\n+ channel_uid=channel[\"uid\"], order_by=\"created_at\"\n+ )\n+ ]\n print(\"Dump succesfull!\")\n print(\"Converting to json.\")\n print(\"Converting succesful, now writing to dump.json\")\n- with open(\"dump.txt\",\"w\") as f:\n- f.write('\\n\\n'.join(result))\n+ with open(\"dump.txt\", \"w\") as f:\n+ f.write(\"\\n\\n\".join(result))\n print(\"Dump written to dump.json\")\n- \n \n-if __name__ == '__main__':\n+\n+if __name__ == \"__main__\":\n asyncio.run(dump_public_channels())\ndiff --git a/src/snek/form/search_user.py b/src/snek/form/search_user.py\nindex 6f431f0..7e946b9 100644\n--- a/src/snek/form/search_user.py\n+++ b/src/snek/form/search_user.py\n@@ -16,4 +16,3 @@ class SearchUserForm(Form):\n action = FormButtonElement(\n name=\"action\", value=\"submit\", text=\"Search\", type=\"button\"\n )\n-\ndiff --git a/src/snek/mapper/__init__.py b/src/snek/mapper/__init__.py\nindex e4c67b0..96053ea 100644\n--- a/src/snek/mapper/__init__.py\n+++ b/src/snek/mapper/__init__.py\n@@ -3,10 +3,10 @@ import functools\n from snek.mapper.channel import ChannelMapper\n from snek.mapper.channel_member import ChannelMemberMapper\n from snek.mapper.channel_message import ChannelMessageMapper\n+from snek.mapper.drive import DriveMapper\n+from snek.mapper.drive_item import DriveItemMapper\n from snek.mapper.notification import NotificationMapper\n from snek.mapper.user import UserMapper\n-from snek.mapper.drive import DriveMapper \n-from snek.mapper.drive_item import DriveItemMapper\n from snek.system.object import Object\n \n \ndiff --git a/src/snek/mapper/drive.py b/src/snek/mapper/drive.py\nindex 970788a..c92c687 100644\n--- a/src/snek/mapper/drive.py\n+++ b/src/snek/mapper/drive.py\n@@ -3,5 +3,5 @@ from snek.system.mapper import BaseMapper\n \n \n class DriveMapper(BaseMapper):\n- table_name = 'drive'\n- model_class = DriveModel \n+ table_name = \"drive\"\n+ model_class = DriveModel\ndiff --git a/src/snek/mapper/drive_item.py b/src/snek/mapper/drive_item.py\nindex c35afe1..3d17a61 100644\n--- a/src/snek/mapper/drive_item.py\n+++ b/src/snek/mapper/drive_item.py\n@@ -1,7 +1,8 @@\n+from snek.model.drive_item import DriveItemModel\n from snek.system.mapper import BaseMapper\n-from snek.model.drive_item import DriveItemModel \n+\n \n class DriveItemMapper(BaseMapper):\n- \n+\n model_class = DriveItemModel\n- table_name = 'drive_item'\n+ table_name = \"drive_item\"\ndiff --git a/src/snek/model/channel.py b/src/snek/model/channel.py\nindex 649299e..183ddb0 100644\n--- a/src/snek/model/channel.py\n+++ b/src/snek/model/channel.py\n@@ -12,11 +12,16 @@ class ChannelModel(BaseModel):\n index = ModelField(name=\"index\", required=True, kind=int, value=1000)\n last_message_on = ModelField(name=\"last_message_on\", required=False, kind=str)\n \n- async def get_last_message(self)->ChannelMessageModel:\n- 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'])):\n- \n- return await self.app.services.channel_message.get(uid=model['uid'])\n+ async def get_last_message(self) -> ChannelMessageModel:\n+ async for model in self.app.services.channel_message.query(\n+ \"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1\",\n+ {\"channel_uid\": self[\"uid\"]},\n+ ):\n+\n+ return await self.app.services.channel_message.get(uid=model[\"uid\"])\n return None\n \n async def get_members(self):\n- return await self.app.services.channel_member.find(channel_uid=self['uid'],deleted_at=None,is_banned=False)\n+ return await self.app.services.channel_member.find(\n+ channel_uid=self[\"uid\"], deleted_at=None, is_banned=False\n+ )\ndiff --git a/src/snek/model/channel_member.py b/src/snek/model/channel_member.py\nindex 9689fa5..54b0418 100644\n--- a/src/snek/model/channel_member.py\n+++ b/src/snek/model/channel_member.py\n@@ -16,25 +16,26 @@ class ChannelMemberModel(BaseModel):\n new_count = ModelField(name=\"new_count\", required=False, kind=int, value=0)\n \n async def get_user(self):\n- return await self.app.services.user.get(uid=self['user_uid'])\n- \n+ return await self.app.services.user.get(uid=self[\"user_uid\"])\n+\n async def get_channel(self):\n- return await self.app.services.channel.get(uid=self['channel_uid'])\n+ return await self.app.services.channel.get(uid=self[\"channel_uid\"])\n \n async def get_name(self):\n channel = await self.get_channel()\n if channel[\"tag\"] == \"dm\":\n user = await self.get_other_dm_user()\n- return user['nick']\n- return channel['name'] or self['label']\n+ return user[\"nick\"]\n+ return channel[\"name\"] or self[\"label\"]\n \n async def get_other_dm_user(self):\n channel = await self.get_channel()\n if channel[\"tag\"] != \"dm\":\n return None\n- \n- async for model in self.app.services.channel_member.find(channel_uid=channel['uid']):\n- if model[\"uid\"] != self['uid']:\n+\n+ async for model in self.app.services.channel_member.find(\n+ channel_uid=channel[\"uid\"]\n+ ):\n+ if model[\"uid\"] != self[\"uid\"]:\n return await self.app.services.user.get(uid=model[\"user_uid\"])\n return await self.get_user()\n- \ndiff --git a/src/snek/model/channel_message.py b/src/snek/model/channel_message.py\nindex 4b84fdc..524a8a4 100644\n--- a/src/snek/model/channel_message.py\n+++ b/src/snek/model/channel_message.py\n@@ -8,8 +8,8 @@ class ChannelMessageModel(BaseModel):\n message = ModelField(name=\"message\", required=True, kind=str)\n html = ModelField(name=\"html\", required=False, kind=str)\n \n- async def get_user(self)->UserModel:\n+ async def get_user(self) -> UserModel:\n return await self.app.services.user.get(uid=self[\"user_uid\"])\n- \n+\n async def get_channel(self):\n- return await self.app.services.channel.get(uid=self[\"channel_uid\"])\n\\ No newline at end of file\n+ return await self.app.services.channel.get(uid=self[\"channel_uid\"])\ndiff --git a/src/snek/model/drive.py b/src/snek/model/drive.py\nindex 3936d97..df17d0f 100644\n--- a/src/snek/model/drive.py\n+++ b/src/snek/model/drive.py\n@@ -1,13 +1,14 @@\n-from snek.system.model import BaseModel,ModelField\n+from snek.system.model import BaseModel, ModelField\n \n \n class DriveModel(BaseModel):\n \n user_uid = ModelField(name=\"user_uid\", required=True)\n- name = ModelField(name='name', required=False, type=str) \n+ name = ModelField(name=\"name\", required=False, type=str)\n \n @property\n async def items(self):\n- async for drive_item in self.app.services.drive_item.find(drive_uid=self['uid']):\n+ async for drive_item in self.app.services.drive_item.find(\n+ drive_uid=self[\"uid\"]\n+ ):\n yield drive_item\n-\ndiff --git a/src/snek/model/drive_item.py b/src/snek/model/drive_item.py\nindex 728cd89..6e28f84 100644\n--- a/src/snek/model/drive_item.py\n+++ b/src/snek/model/drive_item.py\n@@ -1,18 +1,20 @@\n-from snek.system.model import BaseModel,ModelField \n import mimetypes\n \n+from snek.system.model import BaseModel, ModelField\n+\n+\n class DriveItemModel(BaseModel):\n- drive_uid = ModelField(name=\"drive_uid\", required=True,kind=str)\n- name = ModelField(name=\"name\", required=True,kind=str)\n- path = ModelField(name=\"path\", required=True,kind=str)\n- file_type = ModelField(name=\"file_type\", required=True,kind=str) \n- file_size = ModelField(name=\"file_size\", required=True,kind=int)\n- \n+ drive_uid = ModelField(name=\"drive_uid\", required=True, kind=str)\n+ name = ModelField(name=\"name\", required=True, kind=str)\n+ path = ModelField(name=\"path\", required=True, kind=str)\n+ file_type = ModelField(name=\"file_type\", required=True, kind=str)\n+ file_size = ModelField(name=\"file_size\", required=True, kind=int)\n+\n @property\n def extension(self):\n- return self['name'].split('.')[-1]\n- \n- @property \n+ return self[\"name\"].split(\".\")[-1]\n+\n+ @property\n def mime_type(self):\n- mimetype,_ = mimetypes.guess_type(self['name'])\n- return mimetype \n+ mimetype, _ = mimetypes.guess_type(self[\"name\"])\n+ return mimetype\ndiff --git a/src/snek/model/user.py b/src/snek/model/user.py\nindex 0621ecf..a35d890 100644\n--- a/src/snek/model/user.py\n+++ b/src/snek/model/user.py\n@@ -18,10 +18,7 @@ class UserModel(BaseModel):\n regex=r\"^[a-zA-Z0-9_-+/]+$\",\n )\n color = ModelField(\n- name =\"color\",\n- required=True,\n- kind=str\n )\n email = ModelField(\n name=\"email\",\n@@ -33,5 +30,7 @@ class UserModel(BaseModel):\n last_ping = ModelField(name=\"last_ping\", required=False, kind=str)\n \n async def get_channel_members(self):\n- async for channel_member in self.app.services.channel_member.find(user_uid=self['uid'],is_banned=False,deleted_at=None):\n+ async for channel_member in self.app.services.channel_member.find(\n+ user_uid=self[\"uid\"], is_banned=False, deleted_at=None\n+ ):\n yield channel_member\ndiff --git a/src/snek/service/__init__.py b/src/snek/service/__init__.py\nindex e521c7b..4059f77 100644\n--- a/src/snek/service/__init__.py\n+++ b/src/snek/service/__init__.py\n@@ -4,12 +4,12 @@ from snek.service.channel import ChannelService\n from snek.service.channel_member import ChannelMemberService\n from snek.service.channel_message import ChannelMessageService\n from snek.service.chat import ChatService\n+from snek.service.drive import DriveService\n+from snek.service.drive_item import DriveItemService\n from snek.service.notification import NotificationService\n from snek.service.socket import SocketService\n from snek.service.user import UserService\n from snek.service.util import UtilService\n-from snek.service.drive import DriveService\n-from snek.service.drive_item import DriveItemService\n from snek.system.object import Object\n \n \n@@ -26,7 +26,7 @@ def get_services(app):\n \"notification\": NotificationService(app=app),\n \"util\": UtilService(app=app),\n \"drive\": DriveService(app=app),\n- \"drive_item\": DriveItemService(app=app)\n+ \"drive_item\": DriveItemService(app=app),\n }\n )\n \ndiff --git a/src/snek/service/channel.py b/src/snek/service/channel.py\nindex 95ceef7..b90e66f 100644\n--- a/src/snek/service/channel.py\n+++ b/src/snek/service/channel.py\n@@ -1,30 +1,28 @@\n+from datetime import datetime\n+\n+from snek.system.model import now\n from snek.system.service import BaseService\n-from datetime import datetime \n \n-from snek.system.model import now \n \n class ChannelService(BaseService):\n mapper_name = \"channel\"\n \n- async def get(\n- self,\n- uid=None,\n- **kwargs):\n+ async def get(self, uid=None, **kwargs):\n if uid:\n- kwargs['uid'] = uid \n+ kwargs[\"uid\"] = uid\n result = await super().get(**kwargs)\n if result:\n- return result \n- del kwargs['uid']\n- kwargs['name'] = uid \n+ return result\n+ del kwargs[\"uid\"]\n+ kwargs[\"name\"] = uid\n result = await super().get(**kwargs)\n if result:\n return result\n result = await super().get(**kwargs)\n if result:\n return result\n- return None \n+ return None\n return await super().get(**kwargs)\n \n async def create(\n@@ -53,38 +51,34 @@ class ChannelService(BaseService):\n raise Exception(f\"Failed to create channel: {model.errors}.\")\n \n async def get_dm(self, user1, user2):\n- channel_member = await self.services.channel_member.get_dm(\n- user1, user2 \n- )\n+ channel_member = await self.services.channel_member.get_dm(user1, user2)\n if channel_member:\n return await self.get(uid=channel_member[\"channel_uid\"])\n- channel = await self.create(\n- \"DM\", user1, tag=\"dm\" \n- )\n- await self.services.channel_member.create_dm(\n- channel[\"uid\"], user1, user2 \n- )\n- return channel \n+ channel = await self.create(\"DM\", user1, tag=\"dm\")\n+ await self.services.channel_member.create_dm(channel[\"uid\"], user1, user2)\n+ return channel\n \n async def get_users(self, channel_uid):\n- users = []\n async for channel_member in self.services.channel_member.find(\n channel_uid=channel_uid,\n is_banned=False,\n is_muted=False,\n deleted_at=None,\n ):\n- user = await self.services.user.get(uid=channel_member[\"user_uid\"])\n+ user = await self.services.user.get(uid=channel_member[\"user_uid\"])\n if user:\n yield user\n+\n async def get_online_users(self, channel_uid):\n- users = []\n async for user in self.get_users(channel_uid):\n if not user[\"last_ping\"]:\n continue\n \n- if (datetime.fromisoformat(now()) - datetime.fromisoformat(user[\"last_ping\"])).total_seconds() < 20:\n- yield user \n+ if (\n+ datetime.fromisoformat(now())\n+ - datetime.fromisoformat(user[\"last_ping\"])\n+ ).total_seconds() < 20:\n+ yield user\n \n async def get_for_user(self, user_uid):\n async for channel_member in self.services.channel_member.find(\n@@ -93,7 +87,7 @@ class ChannelService(BaseService):\n deleted_at=None,\n ):\n channel = await self.get(uid=channel_member[\"channel_uid\"])\n- yield channel \n+ yield channel\n \n async def ensure_public_channel(self, created_by_uid):\n model = await self.get(is_listed=True, tag=\"public\")\ndiff --git a/src/snek/service/channel_member.py b/src/snek/service/channel_member.py\nindex 5c6c7ee..16d1887 100644\n--- a/src/snek/service/channel_member.py\n+++ b/src/snek/service/channel_member.py\n@@ -6,10 +6,7 @@ class ChannelMemberService(BaseService):\n mapper_name = \"channel_member\"\n \n async def mark_as_read(self, channel_uid, user_uid):\n- channel_member = await self.get(\n- channel_uid=channel_uid,\n- user_uid=user_uid\n- )\n+ channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)\n channel_member[\"new_count\"] = 0\n return await self.save(channel_member)\n \n@@ -24,7 +21,7 @@ class ChannelMemberService(BaseService):\n ):\n model = await self.get(channel_uid=channel_uid, user_uid=user_uid)\n if model:\n- if model['is_banned']:\n+ if model[\"is_banned\"]:\n return False\n return model\n model = await self.new()\n@@ -39,30 +36,32 @@ class ChannelMemberService(BaseService):\n if await self.save(model):\n return model\n raise Exception(f\"Failed to create channel member: {model.errors}.\")\n- \n- async def get_dm(self,from_user, to_user):\n- 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)):\n+\n+ async def get_dm(self, from_user, to_user):\n+ async for model in self.query(\n+ \"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 \",\n+ {\"from_user\": from_user, \"to_user\": to_user},\n+ ):\n return model\n if not from_user == to_user:\n- return None \n- 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)):\n- \n- return model \n- \n+ return None\n+ async for model in self.query(\n+ \"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 \",\n+ {\"from_user\": from_user, \"to_user\": to_user},\n+ ):\n+\n+ return model\n+\n async def get_other_dm_user(self, channel_uid, user_uid):\n channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid)\n- channel = await self.services.channel.get(uid=channel_member['channel_uid'])\n+ channel = await self.services.channel.get(uid=channel_member[\"channel_uid\"])\n if channel[\"tag\"] != \"dm\":\n return None\n async for model in self.services.channel_member.find(channel_uid=channel_uid):\n- if model[\"uid\"] != channel_member['uid']:\n+ if model[\"uid\"] != channel_member[\"uid\"]:\n return await self.services.user.get(uid=model[\"user_uid\"])\n \n- async def create_dm(self,channel_uid, from_user_uid, to_user_uid):\n+ async def create_dm(self, channel_uid, from_user_uid, to_user_uid):\n result = await self.create(channel_uid, from_user_uid)\n await self.create(channel_uid, to_user_uid)\n- return result \n-\n-\n-\n-\n+ return result\ndiff --git a/src/snek/service/channel_message.py b/src/snek/service/channel_message.py\nindex 9683631..f8a000f 100644\n--- a/src/snek/service/channel_message.py\n+++ b/src/snek/service/channel_message.py\n@@ -1,71 +1,93 @@\n from snek.system.service import BaseService\n-import jinja2 \n+\n \n class ChannelMessageService(BaseService):\n mapper_name = \"channel_message\"\n \n async def create(self, channel_uid, user_uid, message):\n model = await self.new()\n- \n+\n model[\"channel_uid\"] = channel_uid\n model[\"user_uid\"] = user_uid\n model[\"message\"] = message\n- \n- context = {\n- \n- }\n- \n- \n- record = model.record \n+\n+ context = {}\n+\n+ record = model.record\n context.update(record)\n user = await self.app.services.user.get(uid=user_uid)\n- context.update(dict(\n- user_uid=user['uid'],\n- username=user['username'],\n- user_nick=user['nick'],\n- color=user['color']\n- ))\n+ context.update(\n+ {\n+ \"user_uid\": user[\"uid\"],\n+ \"username\": user[\"username\"],\n+ \"user_nick\": user[\"nick\"],\n+ \"color\": user[\"color\"],\n+ }\n+ )\n try:\n template = self.app.jinja2_env.get_template(\"message.html\")\n model[\"html\"] = template.render(**context)\n except Exception as ex:\n- print(ex,flush=True)\n- \n+ print(ex, flush=True)\n+\n if await self.save(model):\n return model\n raise Exception(f\"Failed to create channel message: {model.errors}.\")\n- \n+\n async def to_extended_dict(self, message):\n user = await self.services.user.get(uid=message[\"user_uid\"])\n if not user:\n return {}\n return {\n \"uid\": message[\"uid\"],\n- \"color\": user['color'],\n+ \"color\": user[\"color\"],\n \"user_uid\": message[\"user_uid\"],\n \"channel_uid\": message[\"channel_uid\"],\n- \"user_nick\": user['nick'],\n+ \"user_nick\": user[\"nick\"],\n \"message\": message[\"message\"],\n \"created_at\": message[\"created_at\"],\n- \"html\": message['html'],\n- \"username\": user['username'] \n+ \"html\": message[\"html\"],\n+ \"username\": user[\"username\"],\n }\n \n- async def offset(self, channel_uid, page=0, timestamp = None, page_size=30):\n+ async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):\n results = []\n- offset = page * page_size \n+ offset = page * page_size\n try:\n if timestamp:\n- 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)):\n+ async for model in self.query(\n+ \"SELECT * FROM channel_message WHERE channel_uid=:channel_uid AND created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size OFFSET :offset\",\n+ {\n+ \"channel_uid\": channel_uid,\n+ \"page_size\": page_size,\n+ \"offset\": offset,\n+ \"timestamp\": timestamp,\n+ },\n+ ):\n results.append(model)\n elif page > 0:\n- 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 )):\n+ async for model in self.query(\n+ \"SELECT * FROM channel_message WHERE channel_uid=:channel_uid WHERE created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size\",\n+ {\n+ \"channel_uid\": channel_uid,\n+ \"page_size\": page_size,\n+ \"offset\": offset,\n+ \"timestamp\": timestamp,\n+ },\n+ ):\n results.append(model)\n- else: \n- 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)):\n+ else:\n+ async for model in self.query(\n+ \"SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT :page_size OFFSET :offset\",\n+ {\n+ \"channel_uid\": channel_uid,\n+ \"page_size\": page_size,\n+ \"offset\": offset,\n+ },\n+ ):\n results.append(model)\n \n- except: \n+ except:\n pass\n- results.sort(key=lambda x: x['created_at'])\n- return results \n+ results.sort(key=lambda x: x[\"created_at\"])\n+ return results\ndiff --git a/src/snek/service/chat.py b/src/snek/service/chat.py\nindex 8b1f8ad..388d5c0 100644\n--- a/src/snek/service/chat.py\n+++ b/src/snek/service/chat.py\n@@ -1,40 +1,39 @@\n-\n-\n-\n from snek.system.model import now\n from snek.system.service import BaseService\n \n \n class ChatService(BaseService):\n \n- async def send(self,user_uid, channel_uid, message):\n+ async def send(self, user_uid, channel_uid, message):\n channel = await self.services.channel.get(uid=channel_uid)\n if not channel:\n raise Exception(\"Channel not found.\")\n channel_message = await self.services.channel_message.create(\n- channel_uid, \n- user_uid, \n- message\n+ channel_uid, user_uid, message\n )\n channel_message_uid = channel_message[\"uid\"]\n- \n- \n+\n user = await self.services.user.get(uid=user_uid)\n- channel['last_message_on'] = now()\n+ channel[\"last_message_on\"] = now()\n await self.services.channel.save(channel)\n- \n- await self.services.socket.broadcast(channel_uid, dict(\n- message=channel_message[\"message\"],\n- html=channel_message[\"html\"],\n- user_uid=user_uid,\n- color=user['color'],\n- channel_uid=channel_uid,\n- created_at=channel_message[\"created_at\"], \n- updated_at=None,\n- username=user['username'],\n- uid=channel_message['uid'],\n- user_nick=user['nick']\n- ))\n- await self.app.create_task(self.services.notification.create_channel_message(channel_message_uid))\n- \n+\n+ await self.services.socket.broadcast(\n+ channel_uid,\n+ {\n+ \"message\": channel_message[\"message\"],\n+ \"html\": channel_message[\"html\"],\n+ \"user_uid\": user_uid,\n+ \"color\": user[\"color\"],\n+ \"channel_uid\": channel_uid,\n+ \"created_at\": channel_message[\"created_at\"],\n+ \"updated_at\": None,\n+ \"username\": user[\"username\"],\n+ \"uid\": channel_message[\"uid\"],\n+ \"user_nick\": user[\"nick\"],\n+ },\n+ )\n+ await self.app.create_task(\n+ self.services.notification.create_channel_message(channel_message_uid)\n+ )\n+\n return True\ndiff --git a/src/snek/service/drive.py b/src/snek/service/drive.py\nindex b90a959..38035c7 100644\n--- a/src/snek/service/drive.py\n+++ b/src/snek/service/drive.py\n@@ -2,14 +2,90 @@ from snek.system.service import BaseService\n \n \n class DriveService(BaseService):\n- \n+\n mapper_name = \"drive\"\n \n- EXTENSIONS_PICTURES = [\"jpg\",\"jpeg\",\"png\",\"gif\",\"svg\",\"webp\",\"tiff\"]\n- EXTENSIONS_VIDEOS = [\"mp4\",\"m4v\",\"mov\",\"wmv\",\"webm\",\"mkv\",\"mpg\",\"mpeg\",\"avi\",\"ogv\",\"ogg\",\"flv\",\"3gp\",\"3g2\"]\n- EXTENSIONS_ARCHIVES = [\"zip\",\"rar\",\"7z\",\"tar\",\"tar.gz\",\"tar.xz\",\"tar.bz2\",\"tar.lzma\",\"tar.lz\"]\n- EXTENSIONS_AUDIO = [\"mp3\",\"wav\",\"ogg\",\"flac\",\"m4a\",\"wma\",\"aac\",\"opus\",\"aiff\",\"au\",\"mid\",\"midi\"]\n- 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\"]\n+ EXTENSIONS_PICTURES = [\"jpg\", \"jpeg\", \"png\", \"gif\", \"svg\", \"webp\", \"tiff\"]\n+ EXTENSIONS_VIDEOS = [\n+ \"mp4\",\n+ \"m4v\",\n+ \"mov\",\n+ \"wmv\",\n+ \"webm\",\n+ \"mkv\",\n+ \"mpg\",\n+ \"mpeg\",\n+ \"avi\",\n+ \"ogv\",\n+ \"ogg\",\n+ \"flv\",\n+ \"3gp\",\n+ \"3g2\",\n+ ]\n+ EXTENSIONS_ARCHIVES = [\n+ \"zip\",\n+ \"rar\",\n+ \"7z\",\n+ \"tar\",\n+ \"tar.gz\",\n+ \"tar.xz\",\n+ \"tar.bz2\",\n+ \"tar.lzma\",\n+ \"tar.lz\",\n+ ]\n+ EXTENSIONS_AUDIO = [\n+ \"mp3\",\n+ \"wav\",\n+ \"ogg\",\n+ \"flac\",\n+ \"m4a\",\n+ \"wma\",\n+ \"aac\",\n+ \"opus\",\n+ \"aiff\",\n+ \"au\",\n+ \"mid\",\n+ \"midi\",\n+ ]\n+ EXTENSIONS_DOCS = [\n+ \"pdf\",\n+ \"doc\",\n+ \"docx\",\n+ \"xls\",\n+ \"xlsx\",\n+ \"ppt\",\n+ \"pptx\",\n+ \"txt\",\n+ \"md\",\n+ \"json\",\n+ \"csv\",\n+ \"xml\",\n+ \"html\",\n+ \"css\",\n+ \"js\",\n+ \"py\",\n+ \"sql\",\n+ \"rs\",\n+ \"toml\",\n+ \"yml\",\n+ \"yaml\",\n+ \"ini\",\n+ \"conf\",\n+ \"config\",\n+ \"log\",\n+ \"csv\",\n+ \"tsv\",\n+ \"java\",\n+ \"cs\",\n+ \"csproj\",\n+ \"scss\",\n+ \"less\",\n+ \"sass\",\n+ \"json\",\n+ \"lock\",\n+ \"lock.json\",\n+ \"jsonl\",\n+ ]\n \n async def get_drive_name_by_extension(self, extension):\n if extension.startswith(\".\"):\n@@ -26,54 +102,52 @@ class DriveService(BaseService):\n return \"Documents\"\n return \"My Drive\"\n \n- async def get_drive_by_extension(self,user_uid, extension):\n+ async def get_drive_by_extension(self, user_uid, extension):\n name = await self.get_drive_name_by_extension(extension)\n- return await self.get_or_create(user_uid=user_uid,name=name)\n+ return await self.get_or_create(user_uid=user_uid, name=name)\n \n- async def get_by_user(self, user_uid,name=None):\n- kwargs = dict(\n- user_uid = user_uid\n- )\n+ async def get_by_user(self, user_uid, name=None):\n+ kwargs = {\"user_uid\": user_uid}\n async for model in self.find(**kwargs):\n if not name:\n- yield model \n- elif model['name'] == name:\n yield model\n- elif not model['name'] and name == 'My Drive':\n- model['name'] = 'My Drive'\n+ elif model[\"name\"] == name:\n+ yield model\n+ elif not model[\"name\"] and name == \"My Drive\":\n+ model[\"name\"] = \"My Drive\"\n await self.save(model)\n- yield model \n+ yield model\n \n- async def get_or_create(self, user_uid,name=None,extensions=None):\n- kwargs = dict(user_uid=user_uid)\n+ async def get_or_create(self, user_uid, name=None, extensions=None):\n+ kwargs = {\"user_uid\": user_uid}\n if name:\n- kwargs['name'] = name\n+ kwargs[\"name\"] = name\n async for model in self.get_by_user(**kwargs):\n- return model \n+ return model\n \n model = await self.new()\n- model['user_uid'] = user_uid\n- model['name'] = name \n+ model[\"user_uid\"] = user_uid\n+ model[\"name\"] = name\n await self.save(model)\n- return model \n+ return model\n \n async def prepare_default_drives(self):\n async for drive_item in self.services.drive_item.find():\n extension = drive_item.extension\n- drive = await self.get_drive_by_extension(drive_item['user_uid'],extension)\n- if not drive_item['drive_uid'] == drive['uid']:\n- drive_item['drive_uid'] = drive['uid']\n+ drive = await self.get_drive_by_extension(drive_item[\"user_uid\"], extension)\n+ if not drive_item[\"drive_uid\"] == drive[\"uid\"]:\n+ drive_item[\"drive_uid\"] = drive[\"uid\"]\n await self.services.drive_item.save(drive_item)\n- \n+\n async def prepare_default_drives_for_user(self, user_uid):\n- await self.get_or_create(user_uid=user_uid,name=\"My Drive\")\n- await self.get_or_create(user_uid=user_uid,name=\"Shared Drive\")\n- await self.get_or_create(user_uid=user_uid,name=\"Pictures\")\n- await self.get_or_create(user_uid=user_uid,name=\"Videos\")\n- await self.get_or_create(user_uid=user_uid,name=\"Archives\")\n- await self.get_or_create(user_uid=user_uid,name=\"Documents\")\n+ await self.get_or_create(user_uid=user_uid, name=\"My Drive\")\n+ await self.get_or_create(user_uid=user_uid, name=\"Shared Drive\")\n+ await self.get_or_create(user_uid=user_uid, name=\"Pictures\")\n+ await self.get_or_create(user_uid=user_uid, name=\"Videos\")\n+ await self.get_or_create(user_uid=user_uid, name=\"Archives\")\n+ await self.get_or_create(user_uid=user_uid, name=\"Documents\")\n \n async def prepare_all(self):\n await self.prepare_default_drives()\n async for user in self.services.user.find():\n- await self.prepare_default_drives_for_user(user['uid']) \n+ await self.prepare_default_drives_for_user(user[\"uid\"])\ndiff --git a/src/snek/service/drive_item.py b/src/snek/service/drive_item.py\nindex 05a7da8..ce747c1 100644\n--- a/src/snek/service/drive_item.py\n+++ b/src/snek/service/drive_item.py\n@@ -1,19 +1,19 @@\n-from snek.system.service import BaseService \n+from snek.system.service import BaseService\n \n \n class DriveItemService(BaseService):\n \n mapper_name = \"drive_item\"\n \n- async def create(self, drive_uid, name, path, type_,size):\n- model = await self.new() \n- model['drive_uid'] = drive_uid \n- model['name'] = name \n- model['path'] = str(path) \n- model['extension'] = str(name).split(\".\")[-1]\n- model['file_type'] = type_ \n- model['file_size'] = size\n+ async def create(self, drive_uid, name, path, type_, size):\n+ model = await self.new()\n+ model[\"drive_uid\"] = drive_uid\n+ model[\"name\"] = name\n+ model[\"path\"] = str(path)\n+ model[\"extension\"] = str(name).split(\".\")[-1]\n+ model[\"file_type\"] = type_\n+ model[\"file_size\"] = size\n if await self.save(model):\n- return model \n+ return model\n errors = await model.errors\n raise Exception(f\"Failed to create drive item: {errors}.\")\ndiff --git a/src/snek/service/notification.py b/src/snek/service/notification.py\nindex 1392044..a22e8ae 100644\n--- a/src/snek/service/notification.py\n+++ b/src/snek/service/notification.py\n@@ -1,5 +1,6 @@\n-from snek.system.service import BaseService\n from snek.system.model import now\n+from snek.system.service import BaseService\n+\n \n class NotificationService(BaseService):\n mapper_name = \"notification\"\n@@ -7,13 +8,16 @@ class NotificationService(BaseService):\n async def mark_as_read(self, user_uid, channel_message_uid):\n model = await self.get(user_uid, object_uid=channel_message_uid)\n if not model:\n- return False \n- model['read_at'] = now()\n+ return False\n+ model[\"read_at\"] = now()\n await self.save(model)\n- return True \n- \n- async def get_unread_stats(self,user_uid):\n- 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))\n+ return True\n+\n+ async def get_unread_stats(self, user_uid):\n+ await self.query(\n+ \"SELECT object_type, COUNT(*) as count FROM notification WHERE user_uid=:user_uid AND read_at IS NULL GROUP BY object_type\",\n+ {\"user_uid\": user_uid},\n+ )\n \n async def create(self, object_uid, object_type, user_uid, message):\n model = await self.new()\n@@ -37,10 +41,10 @@ class NotificationService(BaseService):\n is_muted=False,\n deleted_at=None,\n ):\n- if not channel_member['new_count']:\n- channel_member['new_count'] = 0\n- channel_member['new_count'] += 1\n- \n+ if not channel_member[\"new_count\"]:\n+ channel_member[\"new_count\"] = 0\n+ channel_member[\"new_count\"] += 1\n+\n usr = await self.services.user.get(uid=channel_member[\"user_uid\"])\n if not usr:\n continue\n@@ -55,7 +59,7 @@ class NotificationService(BaseService):\n )\n try:\n await self.save(model)\n- except Exception as ex:\n+ except Exception:\n raise Exception(f\"Failed to create notification: {model.errors}.\")\n \n self.app.db.commit()\ndiff --git a/src/snek/service/socket.py b/src/snek/service/socket.py\nindex d941321..072a86f 100644\n--- a/src/snek/service/socket.py\n+++ b/src/snek/service/socket.py\n@@ -1,6 +1,4 @@\n from snek.model.user import UserModel\n-\n-\n from snek.system.service import BaseService\n \n \n@@ -9,28 +7,27 @@ class SocketService(BaseService):\n class Socket:\n def __init__(self, ws, user: UserModel):\n self.ws = ws\n- self.is_connected = True \n- self.user = user \n+ self.is_connected = True\n+ self.user = user\n \n async def send_json(self, data):\n if not self.is_connected:\n- return False \n+ return False\n try:\n await self.ws.send_json(data)\n except Exception as ex:\n- print(ex,flush=True)\n+ print(ex, flush=True)\n self.is_connected = False\n- return True \n+ return True\n \n async def close(self):\n if not self.is_connected:\n- return True \n- \n+ return True\n+\n await self.ws.close()\n self.is_connected = False\n- \n- return True \n \n+ return True\n \n def __init__(self, app):\n super().__init__(app)\n@@ -42,32 +39,31 @@ class SocketService(BaseService):\n s = self.Socket(ws, await self.app.services.user.get(uid=user_uid))\n self.sockets.add(s)\n if not self.users.get(user_uid):\n- self.users[user_uid] = set() \n+ self.users[user_uid] = set()\n self.users[user_uid].add(s)\n \n- async def subscribe(self, ws,channel_uid, user_uid):\n+ async def subscribe(self, ws, channel_uid, user_uid):\n return\n- if not channel_uid in self.subscriptions:\n+ if channel_uid not in self.subscriptions:\n self.subscriptions[channel_uid] = set()\n- s = self.Socket(ws,await self.app.services.user.get(uid=user_uid))\n+ s = self.Socket(ws, await self.app.services.user.get(uid=user_uid))\n self.subscriptions[channel_uid].add(s)\n \n async def send_to_user(self, user_uid, message):\n- count = 0 \n- for s in self.users.get(user_uid,[]):\n+ count = 0\n+ for s in self.users.get(user_uid, []):\n if await s.send_json(message):\n- count += 1 \n- return count \n+ count += 1\n+ return count\n \n async def broadcast(self, channel_uid, message):\n- count = 0\n- async for channel_member in self.app.services.channel_member.find(channel_uid=channel_uid):\n- await self.send_to_user(channel_member[\"user_uid\"],message)\n- return True \n- \n+ async for channel_member in self.app.services.channel_member.find(\n+ channel_uid=channel_uid\n+ ):\n+ await self.send_to_user(channel_member[\"user_uid\"], message)\n+ return True\n+\n async def delete(self, ws):\n for s in [sock for sock in self.sockets if sock.ws == ws]:\n await s.close()\n self.sockets.remove(s)\n- \n- \ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nindex 02707b0..6055403 100644\n--- a/src/snek/service/user.py\n+++ b/src/snek/service/user.py\n@@ -1,16 +1,18 @@\n+import pathlib\n+\n from snek.system import security\n from snek.system.service import BaseService\n \n \n class UserService(BaseService):\n mapper_name = \"user\"\n- \n+\n async def search(self, query, **kwargs):\n query = query.strip().lower()\n if not query:\n raise []\n results = []\n- async for result in self.find(username=dict(ilike='%' + query + '%'), **kwargs):\n+ async for result in self.find(username={\"ilike\": \"%\" + query + \"%\"}, **kwargs):\n results.append(result)\n return results\n \n@@ -23,17 +25,32 @@ class UserService(BaseService):\n return True\n \n async def save(self, user):\n- if not user['color']:\n- user['color'] = await self.services.util.random_light_hex_color()\n+ if not user[\"color\"]:\n+ user[\"color\"] = await self.services.util.random_light_hex_color()\n return await super().save(user)\n \n+ async def authenticate(self, username, password):\n+ print(username, password, flush=True)\n+ success = await self.validate_login(username, password)\n+ print(success, flush=True)\n+ if not success:\n+ return None\n+\n+ model = await self.get(username=username, deleted_at=None)\n+ return model\n+\n+ async def get_home_folder(self, user_uid):\n+ folder = pathlib.Path(f\"./drive/{user_uid}\")\n+ if not folder.exists():\n+ folder.mkdir(parents=True, exist_ok=True)\n+ return folder\n \n async def register(self, email, username, password):\n if await self.exists(username=username):\n raise Exception(\"User already exists.\")\n model = await self.new()\n model[\"nick\"] = username\n- model['color'] = await self.services.util.random_light_hex_color()\n+ model[\"color\"] = await self.services.util.random_light_hex_color()\n model.email.value = email\n model.username.value = username\n model.password.value = await security.hash(password)\ndiff --git a/src/snek/service/util.py b/src/snek/service/util.py\nindex 5550a8c..b620d9c 100644\n--- a/src/snek/service/util.py\n+++ b/src/snek/service/util.py\n@@ -1,15 +1,14 @@\n import random\n \n-\n from snek.system.service import BaseService\n \n \n class UtilService(BaseService):\n- \n+\n async def random_light_hex_color(self):\n- \n+\n r = random.randint(128, 255)\n g = random.randint(128, 255)\n b = random.randint(128, 255)\n- \n\\ No newline at end of file\n+\ndiff --git a/src/snek/system/cache.py b/src/snek/system/cache.py\nindex cd9484d..e97fdc3 100644\n--- a/src/snek/system/cache.py\n+++ b/src/snek/system/cache.py\n@@ -20,13 +20,13 @@ class Cache:\n try:\n self.lru.pop(self.lru.index(args))\n except:\n return None\n self.lru.insert(0, args)\n while len(self.lru) > self.max_items:\n self.cache.pop(self.lru[-1])\n self.lru.pop()\n return self.cache[args]\n \n def json_default(self, value):\n@@ -61,7 +61,7 @@ class Cache:\n \n if is_new:\n self.version += 1\n \n async def delete(self, args):\n if args in self.cache:\ndiff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py\nindex ca603d8..82a222e 100644\n--- a/src/snek/system/markdown.py\n+++ b/src/snek/system/markdown.py\n@@ -1,7 +1,7 @@\n \n from types import SimpleNamespace\n-from html import escape\n+\n from app.cache import time_cache_async\n from mistune import HTMLRenderer, Markdown\n from pygments import highlight\n@@ -24,21 +24,21 @@ class MarkdownRenderer(HTMLRenderer):\n def _escape(self, str):\n \n- def get_lexer(self, lang, default='bash'):\n+ def get_lexer(self, lang, default=\"bash\"):\n try:\n return get_lexer_by_name(lang, stripall=True)\n except:\n return get_lexer_by_name(default, stripall=True)\n- \n+\n def block_code(self, code, lang=None, info=None):\n if not lang:\n lang = info\n if not lang:\n- lang = 'bash'\n+ lang = \"bash\"\n lexer = self.get_lexer(lang)\n formatter = html.HtmlFormatter(lineseparator=\"
\")\n result = highlight(code, lexer, formatter)\n- return result \n+ return result\n \n def render(self):\n markdown_string = self.app.template_path.joinpath(self.template).read_text()\ndiff --git a/src/snek/system/middleware.py b/src/snek/system/middleware.py\nindex 2946262..1a6c7e6 100644\n--- a/src/snek/system/middleware.py\n+++ b/src/snek/system/middleware.py\n@@ -20,7 +20,9 @@ async def no_cors_middleware(request, handler):\n async def cors_allow_middleware(request, handler):\n response = await handler(request)\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n- response.headers[\"Access-Control-Allow-Methods\"] = \"GET, POST, OPTIONS, PUT, DELETE\"\n+ response.headers[\"Access-Control-Allow-Methods\"] = (\n+ \"GET, POST, OPTIONS, PUT, DELETE, MOVE, COPY, HEAD, LOCK, UNLOCK, PATCH, PROPFIND\"\n+ )\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n response.headers[\"Access-Control-Allow-Credentials\"] = \"true\"\n return response\n@@ -28,17 +30,12 @@ async def cors_allow_middleware(request, handler):\n \n @web.middleware\n async def cors_middleware(request, handler):\n- if request.method == \"OPTIONS\":\n- response = web.Response()\n- response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n- response.headers[\"Access-Control-Allow-Methods\"] = (\n- \"GET, POST, PUT, DELETE, OPTIONS\"\n- )\n- response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\n- response.headers[\"Access-Control-Allow-Credentials\"] = \"true\"\n- return response\n+ if request.headers.get(\"Allow\"):\n+ return await handler(request)\n \n response = await handler(request)\n+ if request.headers.get(\"Allow\"):\n+ return response\n response.headers[\"Access-Control-Allow-Origin\"] = \"*\"\n response.headers[\"Access-Control-Allow-Methods\"] = \"GET, POST, PUT, DELETE, OPTIONS\"\n response.headers[\"Access-Control-Allow-Headers\"] = \"*\"\ndiff --git a/src/snek/system/profiler.py b/src/snek/system/profiler.py\nindex 193bbb7..e0e5542 100644\n--- a/src/snek/system/profiler.py\n+++ b/src/snek/system/profiler.py\n@@ -1,24 +1,27 @@\n import cProfile\n import pstats\n-import sys \n+import sys\n+\n from aiohttp import web\n+\n profiler = None\n-import io \n+import io\n \n \n @web.middleware\n async def profile_middleware(request, handler):\n- global profiler \n+ global profiler\n if not profiler:\n profiler = cProfile.Profile()\n profiler.enable()\n response = await handler(request)\n profiler.disable()\n stats = pstats.Stats(profiler, stream=sys.stdout)\n- stats.sort_stats('cumulative')\n- stats.print_stats() \n+ stats.sort_stats(\"cumulative\")\n+ stats.print_stats()\n return response\n \n+\n async def profiler_handler(request):\n output = io.StringIO()\n stats = pstats.Stats(profiler, stream=output)\n@@ -27,17 +30,17 @@ async def profiler_handler(request):\n stats.print_stats()\n return web.Response(text=output.getvalue())\n \n+\n class Profiler:\n \n def __init__(self):\n- global profiler \n+ global profiler\n if profiler is None:\n profiler = cProfile.Profile()\n self.profiler = profiler\n \n async def __aenter__(self):\n- self.profiler.enable() \n- \n+ self.profiler.enable()\n+\n async def __aexit__(self, *args, **kwargs):\n self.profiler.disable()\n-\ndiff --git a/src/snek/system/service.py b/src/snek/system/service.py\nindex d65f947..c6d2afc 100644\n--- a/src/snek/system/service.py\n+++ b/src/snek/system/service.py\n@@ -58,7 +58,7 @@ class BaseService:\n raise Exception(f\"Couldn't save model. Errors: f{errors}\")\n \n async def find(self, **kwargs):\n- if not \"_limit\" in kwargs or int(kwargs.get(\"_limit\")) > 30:\n+ if \"_limit\" not in kwargs or int(kwargs.get(\"_limit\")) > 30:\n kwargs[\"_limit\"] = 60\n async for model in self.mapper.find(**kwargs):\n yield model\ndiff --git a/src/snek/system/template.py b/src/snek/system/template.py\nindex 0e71b80..cff807b 100644\n--- a/src/snek/system/template.py\n+++ b/src/snek/system/template.py\n@@ -1,15 +1,21 @@\n+import re\n from types import SimpleNamespace\n-from bs4 import BeautifulSoup\n-import re \n-import emoji\n \n+import emoji\n+from bs4 import BeautifulSoup\n from jinja2 import TemplateSyntaxError, nodes\n from jinja2.ext import Extension\n from jinja2.nodes import Const\n \n-emoji.EMOJI_DATA[''] = {\"en\": \":snek1:\",\"status\":2,\"E\":0.6, \"alias\":[\":snek1:\"]} \n+emoji.EMOJI_DATA[''] = {\n+ \"en\": \":snek1:\",\n+ \"status\": 2,\n+ \"E\": 0.6,\n+ \"alias\": [\":snek1:\"],\n+}\n \n-emoji.EMOJI_DATA[\"\"\"\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n+emoji.EMOJI_DATA[\n+ \"\"\"\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28c0\u28e0\u28e4\u2874\u2836\u28b6\u28de\u28db\u28db\u2873\u28f3\u2836\u28f6\u2876\u28b6\u28b6\u28e6\u28e4\u28c4\u28c0\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28e0\u28e4\u2836\u281a\u28eb\u28f4\u28ec\u2810\u28f6\u28ff\u28ff\u28cf\u28fd\u28ff\u28ff\u28c7\u28bf\u28ef\u28ff\u28ff\u28fb\u28ff\u28ff\u28fe\u28ee\u28f9\u28ff\u28b6\u28c4\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u28c0\u28f4\u28be\u28fb\u28fd\u28fe\u2847\u28a1\u28ff\u28ff\u28c7\u285f\u28ff\u28ff\u28ff\u28fc\u28ff\u28ff\u28ff\u28ff\u28b8\u28ff\u28df\u28ff\u28f7\u28bb\u28ff\u28ff\u28ff\u28f7\u287d\u28e7\u28f9\u287b\u28e6\u2840\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n@@ -64,62 +70,91 @@ emoji.EMOJI_DATA[\"\"\"\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u28f0\u28df\u28fd\u28ff\u28ef\u28ff\u28e3\u28ff\u28ff\u28ff\u28ff\u28ff\u2844\u28bf\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u287f\u281f\u28e1\u28fe\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u284f\u28f0\u28ff\u28ff\u287f\u28b9\u2847\u28fe\u2800\u28ff\u28ff\u28fe\u28ff\u2847\u2800\u2808\u28b7\u28b0\u28ff\u28b8\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28f7\u28fd\u28fb\u28bf\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28f7\u28e6\u28e6\u28c0\u2840\u2808\u281b\u283b\u28bf\u28ce\u28bf\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28f7\u28c4\u2800\u28ff\u2847\u2818\u28e7\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u28f4\u289f\u28fe\u28ff\u28fb\u287f\u28f5\u28ff\u28ff\u28ff\u28ff\u28df\u28fe\u28ef\u280c\u281b\u281b\u281b\u281b\u2809\u2801\u28a0\u28fe\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u287f\u280f\u28f0\u28ff\u28ff\u287f\u2801\u28b8\u2847\u28bf\u2800\u28ff\u28ff\u28ff\u28ff\u2847\u2800\u2820\u2808\u2802\u2883\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28fe\u28ff\u28fb\u287f\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u2847\u2844\u283e\u28f2\u28fe\u28ff\u28cc\u28bf\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28ff\u28e7\u2859\u2807\u2800\u28bb\u2844\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u281b\u281a\u281b\u281b\u281b\u281a\u281b\u281b\u281b\u281b\u281b\u281a\u281b\u281b\u2813\u281a\u2812\u281b\u2812\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281a\u281b\u281b\u281b\u281b\u2813\u281b\u281b\u281b\u2813\u281b\u281b\u281b\u281b\u2813\u281a\u281b\u281b\u2813\u281a\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281a\u281b\u281b\u281b\u281b\u281b\u281b\u2813\u281b\u281a\u281b\u281b\u281b\u281b\u2813\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u281b\u2812\u281a\u281a\u2801\u2800\u2800\u2800\u2800\u2800\u2800\u2800\n-\"\"\"] = {\"en\": \":a1:\",\"status\":2,\"E\":0.6, \"alias\":[\":a1:\"]}\n+\"\"\"\n+] = {\"en\": \":a1:\", \"status\": 2, \"E\": 0.6, \"alias\": [\":a1:\"]}\n+\n \n def set_link_target_blank(text):\n- soup = BeautifulSoup(text, 'html.parser')\n+ soup = BeautifulSoup(text, \"html.parser\")\n \n- for element in soup.find_all(\"a\"): \n- element.attrs['target'] = '_blank'\n- element.attrs['rel'] = 'noopener noreferrer'\n- element.attrs['referrerpolicy'] = 'no-referrer'\n- element.attrs['href'] = element.attrs['href'].strip(\".\").strip(\",\")\n+ for element in soup.find_all(\"a\"):\n+ element.attrs[\"target\"] = \"_blank\"\n+ element.attrs[\"rel\"] = \"noopener noreferrer\"\n+ element.attrs[\"referrerpolicy\"] = \"no-referrer\"\n+ element.attrs[\"href\"] = element.attrs[\"href\"].strip(\".\").strip(\",\")\n \n return str(soup)\n \n+\n def embed_youtube(text):\n- soup = BeautifulSoup(text, 'html.parser') \n- for element in soup.find_all(\"a\"): \n+ soup = BeautifulSoup(text, \"html.parser\")\n+ for element in soup.find_all(\"a\"):\n+ if (\n+ and \"?v=\" in element.attrs[\"href\"]\n+ ):\n video_name = element.attrs[\"href\"].split(\"?v=\")[1].split(\"&\")[0]\n- element.replace_with(BeautifulSoup(embed_template, 'html.parser'))\n+ element.replace_with(BeautifulSoup(embed_template, \"html.parser\"))\n return str(soup)\n \n+\n def embed_image(text):\n- soup = BeautifulSoup(text, 'html.parser') \n- for element in soup.find_all(\"a\"): \n- for extension in [\".png\", \".jpg\", \".jpeg\", \".gif\", \".webp\", \".svg\", \".bmp\", \".tiff\", \".ico\", \".heif\"]:\n- if extension in element.attrs['href'].lower():\n+ soup = BeautifulSoup(text, \"html.parser\")\n+ for element in soup.find_all(\"a\"):\n+ for extension in [\n+ \".png\",\n+ \".jpg\",\n+ \".jpeg\",\n+ \".gif\",\n+ \".webp\",\n+ \".svg\",\n+ \".bmp\",\n+ \".tiff\",\n+ \".ico\",\n+ \".heif\",\n+ ]:\n+ if extension in element.attrs[\"href\"].lower():\n embed_template = f'\"{element.attrs[\"href\"]}\"'\n- element.replace_with(BeautifulSoup(embed_template, 'html.parser'))\n+ element.replace_with(BeautifulSoup(embed_template, \"html.parser\"))\n return str(soup)\n \n+\n def embed_media(text):\n- soup = BeautifulSoup(text, 'html.parser') \n- for element in soup.find_all(\"a\"): \n- for extension in [\".mp4\", \".mp3\", \".wav\", \".ogg\", \".webm\", \".flac\", \".aac\",\".mpg\",\".avi\",\".wmv\"]:\n- if extension in element.attrs['href'].lower():\n+ soup = BeautifulSoup(text, \"html.parser\")\n+ for element in soup.find_all(\"a\"):\n+ for extension in [\n+ \".mp4\",\n+ \".mp3\",\n+ \".wav\",\n+ \".ogg\",\n+ \".webm\",\n+ \".flac\",\n+ \".aac\",\n+ \".mpg\",\n+ \".avi\",\n+ \".wmv\",\n+ ]:\n+ if extension in element.attrs[\"href\"].lower():\n embed_template = f''\n- element.replace_with(BeautifulSoup(embed_template, 'html.parser'))\n+ element.replace_with(BeautifulSoup(embed_template, \"html.parser\"))\n return str(soup)\n \n \n-\n def linkify_https(text):\n- return text \n+ return text\n \n- soup = BeautifulSoup(text, 'html.parser')\n+ soup = BeautifulSoup(text, \"html.parser\")\n \n- for element in soup.find_all(text=True): \n+ for element in soup.find_all(text=True):\n parent = element.parent\n- if parent.name in ['a', 'script', 'style']: \n+ if parent.name in [\"a\", \"script\", \"style\"]:\n continue\n- \n+\n new_text = re.sub(url_pattern, r'\">\\g<0>', element)\n- element.replace_with(BeautifulSoup(new_text, 'html.parser'))\n+ element.replace_with(BeautifulSoup(new_text, \"html.parser\"))\n \n return set_link_target_blank(str(soup))\n \n@@ -140,8 +175,7 @@ class EmojiExtension(Extension):\n ).set_lineno(line_number)\n \n def _to_html(self, md_file, caller):\n- return emoji.emojize(caller(),language='alias')\n-\n+ return emoji.emojize(caller(), language=\"alias\")\n \n \n class LinkifyExtension(Extension):\n@@ -170,6 +204,7 @@ class LinkifyExtension(Extension):\n result = embed_youtube(result)\n return result\n \n+\n class PythonExtension(Extension):\n tags = {\"py3\"}\n \n@@ -186,26 +221,26 @@ class PythonExtension(Extension):\n ).set_lineno(line_number)\n \n def _to_html(self, md_file, caller):\n- \n+\n def fn(source):\n- import subprocess \n- import subprocess \n- import pathlib \n- from pathlib import Path \n- import os \n- import sys \n- import requests\n+ import subprocess\n+\n def system(command):\n if isinstance(command):\n command = command.split(\" \")\n- from io import StringIO \n+ from io import StringIO\n+\n stdout = StringIO()\n- subprocess.run(command,stderr=stdout,stdout=stdout,text=True)\n+ subprocess.run(command, stderr=stdout, stdout=stdout, text=True)\n return stdout.getvalue()\n+\n to_write = []\n+\n def render(text):\n- global to_write \n+ global to_write\n to_write.append(text)\n+\n exec(source)\n return \"\".join(to_write)\n+\n return str(fn(caller()))\ndiff --git a/src/snek/system/terminal.py b/src/snek/system/terminal.py\nindex 80dda17..4d3781e 100644\n--- a/src/snek/system/terminal.py\n+++ b/src/snek/system/terminal.py\n@@ -1,30 +1,27 @@\n import asyncio\n-import aiohttp\n-import aiohttp.web\n import os\n import pty\n-import shlex\n import subprocess\n-import pathlib\n \n commands = {\n- 'alpine': 'docker run -it alpine /bin/sh',\n- 'r': 'docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh',\n+ \"alpine\": \"docker run -it alpine /bin/sh\",\n+ \"r\": \"docker run -v /usr/local/bin:/usr/local/bin -it ubuntu:latest run.sh\",\n }\n \n+\n class TerminalSession:\n- def __init__(self,command):\n+ def __init__(self, command):\n self.master, self.slave = pty.openpty()\n- self.sockets =[]\n- self.history = b''\n- self.history_size = 1024*20\n+ self.sockets = []\n+ self.history = b\"\"\n+ self.history_size = 1024 * 20\n self.process = subprocess.Popen(\n command.split(\" \"),\n stdin=self.slave,\n stdout=self.slave,\n stderr=self.slave,\n bufsize=0,\n- universal_newlines=True\n+ universal_newlines=True,\n )\n \n async def add_websocket(self, ws):\n@@ -35,11 +32,11 @@ class TerminalSession:\n if len(self.sockets) > 1 and self.history:\n start = 0\n try:\n- start = self.history.index(b'\\n')\n+ start = self.history.index(b\"\\n\")\n except ValueError:\n- pass \n+ pass\n await ws.send_bytes(self.history[start:])\n- return \n+ return\n loop = asyncio.get_event_loop()\n while True:\n try:\n@@ -48,9 +45,10 @@ class TerminalSession:\n break\n self.history += data\n if len(self.history) > self.history_size:\n- self.history = self.history[:0-self.history_size]\n+ self.history = self.history[: 0 - self.history_size]\n try:\n+ for ws in self.sockets:\n except:\n self.sockets.remove(ws)\n except Exception:\ndiff --git a/src/snek/system/view.py b/src/snek/system/view.py\nindex 2765642..4a6e7a1 100644\n--- a/src/snek/system/view.py\n+++ b/src/snek/system/view.py\n@@ -12,9 +12,9 @@ class BaseView(web.View):\n return web.HTTPFound(\"/\")\n return await super()._iter()\n \n- @property \n+ @property\n def base_url(self):\n- return str(self.request.url.with_path('').with_query(''))\n+ return str(self.request.url.with_path(\"\").with_query(\"\"))\n \n @property\n def app(self):\ndiff --git a/src/snek/view/about.py b/src/snek/view/about.py\nindex b03f922..aba57ae 100644\n--- a/src/snek/view/about.py\n+++ b/src/snek/view/about.py\n@@ -26,12 +26,14 @@\n \n from snek.system.view import BaseView\n \n+\n class AboutHTMLView(BaseView):\n- \n+\n async def get(self):\n return await self.render_template(\"about.html\")\n \n+\n class AboutMDView(BaseView):\n \n async def get(self):\n- return await self.render_template(\"about.md\")\n\\ No newline at end of file\n+ return await self.render_template(\"about.md\")\ndiff --git a/src/snek/view/avatar.py b/src/snek/view/avatar.py\nindex cbd973c..a85b876 100644\n--- a/src/snek/view/avatar.py\n+++ b/src/snek/view/avatar.py\n@@ -23,11 +23,14 @@\n-from multiavatar import multiavatar\n import uuid\n+\n from aiohttp import web\n+from multiavatar import multiavatar\n+\n from snek.system.view import BaseView\n \n+\n class AvatarView(BaseView):\n login_required = False\n \n@@ -36,6 +39,6 @@ class AvatarView(BaseView):\n if uid == \"unique\":\n uid = str(uuid.uuid4())\n avatar = multiavatar.multiavatar(uid, True, None)\n- response = web.Response(text=avatar, content_type='image/svg+xml')\n- response.headers['Cache-Control'] = f'public, max-age={1337*42}'\n+ response = web.Response(text=avatar, content_type=\"image/svg+xml\")\n+ response.headers[\"Cache-Control\"] = f\"public, max-age={1337*42}\"\n return response\ndiff --git a/src/snek/view/docs.py b/src/snek/view/docs.py\nindex 592d1a2..bb63413 100644\n--- a/src/snek/view/docs.py\n+++ b/src/snek/view/docs.py\n@@ -1,35 +1,37 @@\n- \n- \n- \n- \n- \n+\n+\n+\n+\n+\n from snek.system.view import BaseView\n \n+\n class DocsHTMLView(BaseView):\n \n async def get(self):\n return await self.render_template(\"docs.html\")\n \n+\n class DocsMDView(BaseView):\n \n async def get(self):\n- return await self.render_template(\"docs.md\")\n\\ No newline at end of file\n+ return await self.render_template(\"docs.md\")\ndiff --git a/src/snek/view/drive.py b/src/snek/view/drive.py\nindex 1026cf7..853cdb2 100644\n--- a/src/snek/view/drive.py\n+++ b/src/snek/view/drive.py\n@@ -1,6 +1,8 @@\n-from snek.system.view import BaseView\n from aiohttp import web\n \n+from snek.system.view import BaseView\n+\n+\n class DriveView(BaseView):\n \n login_required = True\n@@ -14,21 +16,22 @@ class DriveView(BaseView):\n drive_items = []\n async for item in drive.items:\n record = item.record\n- record['url'] = '/drive.bin/' + record['uid'] + '.' + item.extension\n+ record[\"url\"] = \"/drive.bin/\" + record[\"uid\"] + \".\" + item.extension\n drive_items.append(record)\n return web.json_response(drive_items)\n \n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n \n- \n drives = []\n- async for drive in self.services.drive.get_by_user(user['uid']):\n+ async for drive in self.services.drive.get_by_user(user[\"uid\"]):\n record = drive.record\n- record['items'] = []\n+ record[\"items\"] = []\n async for item in drive.items:\n drive_item_record = item.record\n- drive_item_record['url'] = '/drive.bin/' + drive_item_record['uid'] + '.' + item.extension\n- record['items'].append(item.record)\n+ drive_item_record[\"url\"] = (\n+ \"/drive.bin/\" + drive_item_record[\"uid\"] + \".\" + item.extension\n+ )\n+ record[\"items\"].append(item.record)\n drives.append(record)\n- \n+\n return web.json_response(drives)\ndiff --git a/src/snek/view/index.py b/src/snek/view/index.py\nindex 3e62518..6ad6e70 100644\n--- a/src/snek/view/index.py\n+++ b/src/snek/view/index.py\n@@ -12,6 +12,7 @@\n \n from snek.system.view import BaseView\n \n+\n class IndexView(BaseView):\n async def get(self):\n- return await self.render_template(\"index.html\")\n\\ No newline at end of file\n+ return await self.render_template(\"index.html\")\ndiff --git a/src/snek/view/login.py b/src/snek/view/login.py\nindex 5028a7a..be79328 100644\n--- a/src/snek/view/login.py\n+++ b/src/snek/view/login.py\n@@ -7,9 +7,11 @@\n \n from aiohttp import web\n+\n from snek.form.login import LoginForm\n from snek.system.view import BaseFormView\n \n+\n class LoginView(BaseFormView):\n form = LoginForm\n \n@@ -18,17 +20,23 @@ class LoginView(BaseFormView):\n return web.HTTPFound(\"/web.html\")\n if self.request.path.endswith(\".json\"):\n return await super().get()\n- return await self.render_template(\"login.html\", {\"form\": await self.form(app=self.app).to_json()})\n+ return await self.render_template(\n+ \"login.html\", {\"form\": await self.form(app=self.app).to_json()}\n+ )\n \n async def submit(self, form):\n if await form.is_valid:\n- user = await self.services.user.get(username=form['username'], deleted_at=None)\n+ user = await self.services.user.get(\n+ username=form[\"username\"], deleted_at=None\n+ )\n await self.services.user.save(user)\n- self.session.update({\n- \"logged_in\": True,\n- \"username\": user['username'],\n- \"uid\": user[\"uid\"],\n- \"color\": user[\"color\"]\n- })\n+ self.session.update(\n+ {\n+ \"logged_in\": True,\n+ \"username\": user[\"username\"],\n+ \"uid\": user[\"uid\"],\n+ \"color\": user[\"color\"],\n+ }\n+ )\n return {\"redirect_url\": \"/web.html\"}\n- return {\"is_valid\": False}\n\\ No newline at end of file\n+ return {\"is_valid\": False}\ndiff --git a/src/snek/view/login_form.py b/src/snek/view/login_form.py\nindex 230d334..acf7c75 100644\n--- a/src/snek/view/login_form.py\n+++ b/src/snek/view/login_form.py\n@@ -20,4 +20,4 @@ class LoginFormView(BaseFormView):\n self.session[\"username\"] = form.username.value\n self.session[\"uid\"] = form.uid.value\n return {\"redirect_url\": \"/web.html\"}\n- return {\"is_valid\": False}\n\\ No newline at end of file\n+ return {\"is_valid\": False}\ndiff --git a/src/snek/view/logout.py b/src/snek/view/logout.py\nindex 57b92a3..42016d8 100644\n--- a/src/snek/view/logout.py\n+++ b/src/snek/view/logout.py\n@@ -15,10 +15,10 @@\n@@ -29,6 +29,7 @@\n \n \n from aiohttp import web\n+\n from snek.system.view import BaseView\n \n \n@@ -52,4 +53,4 @@ class LogoutView(BaseView):\n del self.session[\"username\"]\n except KeyError:\n pass\n- return await self.json_response({\"redirect_url\": self.redirect_url})\n\\ No newline at end of file\n+ return await self.json_response({\"redirect_url\": self.redirect_url})\ndiff --git a/src/snek/view/register.py b/src/snek/view/register.py\nindex 6e49506..7fbce9d 100644\n--- a/src/snek/view/register.py\n+++ b/src/snek/view/register.py\n@@ -2,14 +2,16 @@\n \n \n \n \n from aiohttp import web\n+\n from snek.form.register import RegisterForm\n from snek.system.view import BaseFormView\n \n+\n class RegisterView(BaseFormView):\n form = RegisterForm\n \n@@ -18,16 +20,20 @@ class RegisterView(BaseFormView):\n return web.HTTPFound(\"/web.html\")\n if self.request.path.endswith(\".json\"):\n return await super().get()\n- return await self.render_template(\"register.html\", {\"form\": await self.form(app=self.app).to_json()})\n+ return await self.render_template(\n+ \"register.html\", {\"form\": await self.form(app=self.app).to_json()}\n+ )\n \n async def submit(self, form):\n result = await self.app.services.user.register(\n form.email.value, form.username.value, form.password.value\n )\n- self.request.session.update({\n- \"uid\": result[\"uid\"],\n- \"username\": result[\"username\"],\n- \"logged_in\": True,\n- \"color\": result[\"color\"]\n- })\n- return {\"redirect_url\": \"/web.html\"}\n\\ No newline at end of file\n+ self.request.session.update(\n+ {\n+ \"uid\": result[\"uid\"],\n+ \"username\": result[\"username\"],\n+ \"logged_in\": True,\n+ \"color\": result[\"color\"],\n+ }\n+ )\n+ return {\"redirect_url\": \"/web.html\"}\ndiff --git a/src/snek/view/register_form.py b/src/snek/view/register_form.py\nindex 5ce42a7..7b98647 100644\n--- a/src/snek/view/register_form.py\n+++ b/src/snek/view/register_form.py\n@@ -2,7 +2,7 @@\n \n \n \n@@ -13,10 +13,10 @@\n@@ -28,6 +28,7 @@\n from snek.form.register import RegisterForm\n from snek.system.view import BaseFormView\n \n+\n class RegisterFormView(BaseFormView):\n form = RegisterForm\n \n@@ -35,10 +36,12 @@ class RegisterFormView(BaseFormView):\n result = await self.app.services.user.register(\n form.email.value, form.username.value, form.password.value\n )\n- self.request.session.update({\n- \"uid\": result[\"uid\"],\n- \"username\": result[\"username\"],\n- \"logged_in\": True,\n- \"color\": result[\"color\"]\n- })\n- return {\"redirect_url\": \"/web.html\"}\n\\ No newline at end of file\n+ self.request.session.update(\n+ {\n+ \"uid\": result[\"uid\"],\n+ \"username\": result[\"username\"],\n+ \"logged_in\": True,\n+ \"color\": result[\"color\"],\n+ }\n+ )\n+ return {\"redirect_url\": \"/web.html\"}\ndiff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py\nindex 27a8dcf..19c98d4 100644\n--- a/src/snek/view/rpc.py\n+++ b/src/snek/view/rpc.py\n@@ -7,41 +7,44 @@\n \n \n-from aiohttp import web \n-from snek.system.view import BaseView\n-import traceback\n import json\n-from snek.system.model import now \n+import traceback\n+\n+from aiohttp import web\n+\n+from snek.system.model import now\n from snek.system.profiler import Profiler\n+from snek.system.view import BaseView\n+\n \n class RPCView(BaseView):\n \n class RPCApi:\n def __init__(self, view, ws):\n- self.view = view \n+ self.view = view\n self.app = self.view.app\n self.services = self.app.services\n- self.ws = ws \n+ self.ws = ws\n \n @property\n def user_uid(self):\n return self.view.session.get(\"uid\")\n \n- @property \n+ @property\n def request(self):\n- return self.view.request \n- \n+ return self.view.request\n+\n def _require_login(self):\n if not self.is_logged_in:\n raise Exception(\"Not logged in\")\n \n- @property \n+ @property\n def is_logged_in(self):\n return self.view.session.get(\"logged_in\", False)\n \n async def mark_as_read(self, channel_uid):\n self._require_login()\n- await self.services.channel_member.mark_as_read(channel_uid, self.user_uid) \n+ await self.services.channel_member.mark_as_read(channel_uid, self.user_uid)\n return True\n \n async def login(self, username, password):\n@@ -54,16 +57,26 @@ class RPCView(BaseView):\n self.view.session[\"username\"] = user[\"username\"]\n self.view.session[\"user_nick\"] = user[\"nick\"]\n record = user.record\n- del record['password']\n- del record['deleted_at']\n- await self.services.socket.add(self.ws,self.view.request.session.get('uid'))\n- async for subscription in self.services.channel_member.find(user_uid=self.view.request.session.get(\"uid\"), deleted_at=None, is_banned=False):\n- await self.services.socket.subscribe(self.ws, subscription[\"channel_uid\"], self.view.request.session.get(\"uid\"))\n- return record \n-\n- async def search_user(self, query): \n+ del record[\"password\"]\n+ del record[\"deleted_at\"]\n+ await self.services.socket.add(\n+ self.ws, self.view.request.session.get(\"uid\")\n+ )\n+ async for subscription in self.services.channel_member.find(\n+ user_uid=self.view.request.session.get(\"uid\"),\n+ deleted_at=None,\n+ is_banned=False,\n+ ):\n+ await self.services.socket.subscribe(\n+ self.ws,\n+ subscription[\"channel_uid\"],\n+ self.view.request.session.get(\"uid\"),\n+ )\n+ return record\n+\n+ async def search_user(self, query):\n self._require_login()\n- return [user['username'] for user in await self.services.user.search(query)]\n+ return [user[\"username\"] for user in await self.services.user.search(query)]\n \n async def get_user(self, user_uid):\n self._require_login()\n@@ -71,46 +84,56 @@ class RPCView(BaseView):\n user_uid = self.user_uid\n user = await self.services.user.get(uid=user_uid)\n record = user.record\n- del record['password']\n- del record['deleted_at']\n+ del record[\"password\"]\n+ del record[\"deleted_at\"]\n if user_uid != user[\"uid\"]:\n- del record['email']\n- return record \n+ del record[\"email\"]\n+ return record\n \n- async def get_messages(self, channel_uid, offset=0,timestamp = None):\n+ async def get_messages(self, channel_uid, offset=0, timestamp=None):\n self._require_login()\n messages = []\n- for message in await self.services.channel_message.offset(channel_uid, offset or 0,timestamp or None):\n- extended_dict = await self.services.channel_message.to_extended_dict(message)\n+ for message in await self.services.channel_message.offset(\n+ channel_uid, offset or 0, timestamp or None\n+ ):\n+ extended_dict = await self.services.channel_message.to_extended_dict(\n+ message\n+ )\n messages.append(extended_dict)\n return messages\n \n async def get_channels(self):\n self._require_login()\n channels = []\n- async for subscription in self.services.channel_member.find(user_uid=self.user_uid, is_banned=False):\n- channel = await self.services.channel.get(uid=subscription['channel_uid'])\n+ async for subscription in self.services.channel_member.find(\n+ user_uid=self.user_uid, is_banned=False\n+ ):\n+ channel = await self.services.channel.get(\n+ uid=subscription[\"channel_uid\"]\n+ )\n last_message = await channel.get_last_message()\n- color = None \n+ color = None\n if last_message:\n last_message_user = await last_message.get_user()\n- color = last_message_user['color']\n- channels.append({\n- \"name\": subscription[\"label\"],\n- \"uid\": subscription[\"channel_uid\"],\n- \"tag\": channel[\"tag\"],\n- \"new_count\": subscription[\"new_count\"],\n- \"is_moderator\": subscription[\"is_moderator\"],\n- \"is_read_only\": subscription[\"is_read_only\"],\n- 'new_count': subscription['new_count'],\n- 'color': color \n- })\n+ color = last_message_user[\"color\"]\n+ channels.append(\n+ {\n+ \"name\": subscription[\"label\"],\n+ \"uid\": subscription[\"channel_uid\"],\n+ \"tag\": channel[\"tag\"],\n+ \"new_count\": subscription[\"new_count\"],\n+ \"is_moderator\": subscription[\"is_moderator\"],\n+ \"is_read_only\": subscription[\"is_read_only\"],\n+ \"new_count\": subscription[\"new_count\"],\n+ \"color\": color,\n+ }\n+ )\n return channels\n \n async def send_message(self, channel_uid, message):\n self._require_login()\n await self.services.chat.send(self.user_uid, channel_uid, message)\n- return True \n+ return True\n \n async def echo(self, *args):\n self._require_login()\n@@ -118,29 +141,47 @@ class RPCView(BaseView):\n \n async def query(self, *args):\n self._require_login()\n- query = args[0] \n+ query = args[0]\n lowercase = query.lower()\n- if any(keyword in lowercase for keyword in [\"drop\", \"alter\", \"update\", \"delete\", \"replace\", \"insert\", \"truncate\"]) and 'select' not in lowercase:\n+ if (\n+ any(\n+ keyword in lowercase\n+ for keyword in [\n+ \"drop\",\n+ \"alter\",\n+ \"update\",\n+ \"delete\",\n+ \"replace\",\n+ \"insert\",\n+ \"truncate\",\n+ ]\n+ )\n+ and \"select\" not in lowercase\n+ ):\n raise Exception(\"Not allowed\")\n- records = [dict(record) async for record in self.services.channel.query(args[0])]\n+ records = [\n+ dict(record) async for record in self.services.channel.query(args[0])\n+ ]\n for record in records:\n try:\n- del record['email']\n+ del record[\"email\"]\n except KeyError:\n- pass \n+ pass\n try:\n del record[\"password\"]\n except KeyError:\n- pass \n+ pass\n try:\n- del record['message']\n+ del record[\"message\"]\n except:\n pass\n try:\n- del record['html']\n- except: \n+ del record[\"html\"]\n+ except:\n pass\n- return [dict(record) async for record in self.services.channel.query(args[0])]\n+ return [\n+ dict(record) async for record in self.services.channel.query(args[0])\n+ ]\n \n async def __call__(self, data):\n try:\n@@ -150,30 +191,43 @@ class RPCView(BaseView):\n raise Exception(\"Not allowed\")\n args = data.get(\"args\") or []\n if hasattr(super(), method_name) or not hasattr(self, method_name):\n- return await self._send_json({\"callId\": call_id, \"data\": \"Not allowed\"})\n+ return await self._send_json(\n+ {\"callId\": call_id, \"data\": \"Not allowed\"}\n+ )\n method = getattr(self, method_name.replace(\".\", \"_\"), None)\n if not method:\n raise Exception(\"Method not found\")\n- success = True \n+ success = True\n try:\n result = await method(*args)\n except Exception as ex:\n result = {\"exception\": str(ex), \"traceback\": traceback.format_exc()}\n success = False\n if result != \"noresponse\":\n- await self._send_json({\"callId\": call_id, \"success\": success, \"data\": result})\n+ await self._send_json(\n+ {\"callId\": call_id, \"success\": success, \"data\": result}\n+ )\n except Exception as ex:\n print(str(ex), flush=True)\n- await self._send_json({\"callId\": call_id, \"success\": False, \"data\": str(ex)})\n+ await self._send_json(\n+ {\"callId\": call_id, \"success\": False, \"data\": str(ex)}\n+ )\n \n async def _send_json(self, obj):\n await self.ws.send_str(json.dumps(obj, default=str))\n- \n \n async def get_online_users(self, channel_uid):\n self._require_login()\n- \n- 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)]\n+\n+ return [\n+ {\n+ \"uid\": record[\"uid\"],\n+ \"username\": record[\"username\"],\n+ \"nick\": record[\"nick\"],\n+ \"last_ping\": record[\"last_ping\"],\n+ }\n+ async for record in self.services.channel.get_online_users(channel_uid)\n+ ]\n \n async def echo(self, obj):\n await self.ws.send_json(obj)\n@@ -182,12 +236,20 @@ class RPCView(BaseView):\n async def get_users(self, channel_uid):\n self._require_login()\n \n- 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)]\n+ return [\n+ {\n+ \"uid\": record[\"uid\"],\n+ \"username\": record[\"username\"],\n+ \"nick\": record[\"nick\"],\n+ \"last_ping\": record[\"last_ping\"],\n+ }\n+ async for record in self.services.channel.get_users(channel_uid)\n+ ]\n \n async def ping(self, callId, *args):\n if self.user_uid:\n user = await self.services.user.get(uid=self.user_uid)\n- user['last_ping'] = now()\n+ user[\"last_ping\"] = now()\n await self.services.user.save(user)\n return {\"pong\": args}\n \n@@ -196,8 +258,14 @@ class RPCView(BaseView):\n await ws.prepare(self.request)\n if self.request.session.get(\"logged_in\"):\n await self.services.socket.add(ws, self.request.session.get(\"uid\"))\n- async for subscription in self.services.channel_member.find(user_uid=self.request.session.get(\"uid\"), deleted_at=None, is_banned=False):\n- await self.services.socket.subscribe(ws, subscription[\"channel_uid\"], self.request.session.get(\"uid\"))\n+ async for subscription in self.services.channel_member.find(\n+ user_uid=self.request.session.get(\"uid\"),\n+ deleted_at=None,\n+ is_banned=False,\n+ ):\n+ await self.services.socket.subscribe(\n+ ws, subscription[\"channel_uid\"], self.request.session.get(\"uid\")\n+ )\n rpc = RPCView.RPCApi(self, ws)\n async for msg in ws:\n if msg.type == web.WSMsgType.TEXT:\n@@ -209,7 +277,7 @@ class RPCView(BaseView):\n await self.services.socket.delete(ws)\n break\n elif msg.type == web.WSMsgType.ERROR:\n- pass \n+ pass\n elif msg.type == web.WSMsgType.CLOSE:\n- pass \n+ pass\n return ws\ndiff --git a/src/snek/view/search_user.py b/src/snek/view/search_user.py\nindex 347d3b2..d97a4b6 100644\n--- a/src/snek/view/search_user.py\n+++ b/src/snek/view/search_user.py\n@@ -8,17 +8,17 @@\n \n@@ -28,7 +28,6 @@\n \n \n-from aiohttp import web\n from snek.form.search_user import SearchUserForm\n from snek.system.view import BaseFormView\n \n@@ -40,14 +39,17 @@ class SearchUserView(BaseFormView):\n users = []\n query = self.request.query.get(\"query\")\n if query:\n- users = [user.record for user in await self.app.services.user.search(query)]\n+ users = [user.record for user in await self.app.services.user.search(query)]\n \n if self.request.path.endswith(\".json\"):\n return await super().get()\n current_user = await self.app.services.user.get(uid=self.session.get(\"uid\"))\n- return await self.render_template(\"search_user.html\", {\"users\": users, \"query\": query or '','current_user': current_user})\n+ return await self.render_template(\n+ \"search_user.html\",\n+ {\"users\": users, \"query\": query or \"\", \"current_user\": current_user},\n+ )\n \n async def submit(self, form):\n if await form.is_valid:\n- return {\"redirect_url\": \"/search-user.html?query=\" + form['username']}\n+ return {\"redirect_url\": \"/search-user.html?query=\" + form[\"username\"]}\n return {\"is_valid\": False}\ndiff --git a/src/snek/view/status.py b/src/snek/view/status.py\nindex 117942a..4675572 100644\n--- a/src/snek/view/status.py\n+++ b/src/snek/view/status.py\n@@ -25,17 +25,18 @@\n \n from snek.system.view import BaseView\n \n+\n class StatusView(BaseView):\n async def get(self):\n memberships = []\n user = {}\n- \n+\n user_id = self.session.get(\"uid\")\n if user_id:\n user = await self.app.services.user.get(uid=user_id)\n if not user:\n return await self.json_response({\"error\": \"User not found\"}, status=404)\n- \n+\n async for model in self.app.services.channel_member.find(\n user_uid=user_id, deleted_at=None, is_banned=False\n ):\n@@ -69,4 +70,4 @@ class StatusView(BaseView):\n self.app.cache.cache, None\n ),\n }\n- )\n\\ No newline at end of file\n+ )\ndiff --git a/src/snek/view/terminal.py b/src/snek/view/terminal.py\nindex 6596f2c..d3af9b0 100644\n--- a/src/snek/view/terminal.py\n+++ b/src/snek/view/terminal.py\n@@ -1,15 +1,17 @@\n-from snek.system.view import BaseView \n-import aiohttp \n-import asyncio\n-from snek.system.terminal import TerminalSession\n import pathlib\n \n+import aiohttp\n+\n+from snek.system.terminal import TerminalSession\n+from snek.system.view import BaseView\n+\n+\n class TerminalSocketView(BaseView):\n- \n+\n login_required = True\n \n user_sessions = {}\n- \n+\n async def prepare_drive(self):\n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n root = pathlib.Path(\"drive\").joinpath(user[\"uid\"])\n@@ -19,40 +21,34 @@ class TerminalSocketView(BaseView):\n destination_path = root.joinpath(path.name)\n if not path.is_dir():\n destination_path.write_bytes(path.read_bytes())\n- return root \n- \n- async def get(self):\n- \n+ return root\n \n+ async def get(self):\n \n ws = aiohttp.web.WebSocketResponse()\n await ws.prepare(self.request)\n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n root = await self.prepare_drive()\n- \n-\n- \n \n command = f\"docker run -v ./{root}/:/root -it --memory 512M --cpus=0.5 -w /root snek_ubuntu /bin/bash\"\n \n session = self.user_sessions.get(user[\"uid\"])\n if not session:\n self.user_sessions[user[\"uid\"]] = TerminalSession(command=command)\n- session = self.user_sessions[user[\"uid\"]] \n+ session = self.user_sessions[user[\"uid\"]]\n await session.add_websocket(ws)\n \n async for msg in ws:\n if msg.type == aiohttp.WSMsgType.BINARY:\n await session.write_input(msg.data.decode())\n \n- \n return ws\n \n+\n class TerminalView(BaseView):\n \n login_required = True\n \n async def get(self):\n- request = self.request\n- return await self.request.app.render_template('terminal.html',self.request)\n+ return await self.request.app.render_template(\"terminal.html\", self.request)\ndiff --git a/src/snek/view/threads.py b/src/snek/view/threads.py\nindex 4f3fe8c..bc923c6 100644\n--- a/src/snek/view/threads.py\n+++ b/src/snek/view/threads.py\n@@ -1,5 +1,6 @@\n from snek.system.view import BaseView\n \n+\n class ThreadsView(BaseView):\n \n async def get(self):\n@@ -12,22 +13,25 @@ class ThreadsView(BaseView):\n if not last_message:\n continue\n \n- thread[\"uid\"] = channel['uid']\n+ thread[\"uid\"] = channel[\"uid\"]\n thread[\"name\"] = await channel_member.get_name()\n thread[\"new_count\"] = channel_member[\"new_count\"]\n thread[\"last_message_on\"] = channel[\"last_message_on\"]\n- thread['created_at'] = thread['last_message_on']\n+ thread[\"created_at\"] = thread[\"last_message_on\"]\n \n- \n thread[\"last_message_text\"] = last_message[\"message\"]\n- thread['last_message_user_uid'] = last_message[\"user_uid\"]\n- user_last_message = await self.app.services.user.get(uid=last_message[\"user_uid\"])\n- if channel['tag'] == \"dm\":\n- thread['name_color'] = user_last_message['color']\n- thread['last_message_user_color'] = user_last_message['color'] \n+ thread[\"last_message_user_uid\"] = last_message[\"user_uid\"]\n+ user_last_message = await self.app.services.user.get(\n+ uid=last_message[\"user_uid\"]\n+ )\n+ if channel[\"tag\"] == \"dm\":\n+ thread[\"name_color\"] = user_last_message[\"color\"]\n+ thread[\"last_message_user_color\"] = user_last_message[\"color\"]\n threads.append(thread)\n- \n- threads.sort(key=lambda x: x['last_message_on'] or '', reverse=True)\n \n- return await self.render_template(\"threads.html\", dict(threads=threads,user=user))\n+ threads.sort(key=lambda x: x[\"last_message_on\"] or \"\", reverse=True)\n+\n+ return await self.render_template(\n+ \"threads.html\", {\"threads\": threads, \"user\": user}\n+ )\ndiff --git a/src/snek/view/upload.py b/src/snek/view/upload.py\nindex 8884d72..b32d94e 100644\n--- a/src/snek/view/upload.py\n+++ b/src/snek/view/upload.py\n@@ -1,31 +1,36 @@\n \n \n \n \n-from snek.system.view import BaseView\n-import aiofiles\n import pathlib\n-from aiohttp import web\n import uuid\n \n+import aiofiles\n+from aiohttp import web\n+\n+from snek.system.view import BaseView\n+\n UPLOAD_DIR = pathlib.Path(\"./drive\")\n \n+\n class UploadView(BaseView):\n \n async def get(self):\n uid = self.request.match_info.get(\"uid\")\n drive_item = await self.services.drive_item.get(uid)\n response = web.FileResponse(drive_item[\"path\"])\n- response.headers['Cache-Control'] = f'public, max-age={1337*420}'\n- response.headers['Content-Disposition'] = f'attachment; filename=\"{drive_item[\"name\"]}\"'\n+ response.headers[\"Cache-Control\"] = f\"public, max-age={1337*420}\"\n+ response.headers[\"Content-Disposition\"] = (\n+ f'attachment; filename=\"{drive_item[\"name\"]}\"'\n+ )\n return response\n \n async def post(self):\n@@ -36,7 +41,9 @@ class UploadView(BaseView):\n \n channel_uid = None\n \n- drive = await self.services.drive.get_or_create(user_uid=self.request.session.get(\"uid\"))\n+ drive = await self.services.drive.get_or_create(\n+ user_uid=self.request.session.get(\"uid\")\n+ )\n \n extension_types = {\n \".jpg\": \"image\",\n@@ -47,7 +54,7 @@ class UploadView(BaseView):\n \".mp3\": \"audio\",\n \".pdf\": \"document\",\n \".doc\": \"document\",\n- \".docx\": \"document\"\n+ \".docx\": \"document\",\n }\n \n while field := await reader.next():\n@@ -58,32 +65,45 @@ class UploadView(BaseView):\n filename = field.filename\n if not filename:\n continue\n- \n+\n name = str(uuid.uuid4()) + pathlib.Path(filename).suffix\n \n file_path = pathlib.Path(UPLOAD_DIR).joinpath(name)\n files.append(file_path)\n \n- async with aiofiles.open(str(file_path.absolute()), 'wb') as f:\n+ async with aiofiles.open(str(file_path.absolute()), \"wb\") as f:\n while chunk := await field.read_chunk():\n await f.write(chunk)\n \n drive_item = await self.services.drive_item.create(\n- drive[\"uid\"], filename, str(file_path.absolute()), file_path.stat().st_size, file_path.suffix\n+ drive[\"uid\"],\n+ filename,\n+ str(file_path.absolute()),\n+ file_path.stat().st_size,\n+ file_path.suffix,\n )\n- \n- type_ = \"unknown\"\n+\n extension = \".\" + filename.split(\".\")[-1]\n if extension in extension_types:\n- type_ = extension_types[extension] \n- \n+ extension_types[extension]\n+\n await self.services.drive_item.save(drive_item)\n- response = \"Uploaded [\" + filename + \"](/drive.bin/\" + drive_item[\"uid\"] + \")\"\n- response = \"[\" + filename + \"](/drive.bin/\" + drive_item[\"uid\"] + extension + \")\"\n+ response = (\n+ \"Uploaded [\" + filename + \"](/drive.bin/\" + drive_item[\"uid\"] + \")\"\n+ )\n+ response = (\n+ \"[\" + filename + \"](/drive.bin/\" + drive_item[\"uid\"] + extension + \")\"\n+ )\n \n await self.services.chat.send(\n self.request.session.get(\"uid\"), channel_uid, response\n )\n \n- return web.json_response({\"message\": \"Files uploaded successfully\", \"files\": [str(file) for file in files], \"channel_uid\": channel_uid})\n+ return web.json_response(\n+ {\n+ \"message\": \"Files uploaded successfully\",\n+ \"files\": [str(file) for file in files],\n+ \"channel_uid\": channel_uid,\n+ }\n+ )\ndiff --git a/src/snek/view/web.py b/src/snek/view/web.py\nindex b9271c3..111f76c 100644\n--- a/src/snek/view/web.py\n+++ b/src/snek/view/web.py\n@@ -25,37 +25,55 @@\n \n from aiohttp import web\n+\n from snek.system.view import BaseView\n \n+\n class WebView(BaseView):\n login_required = True\n \n async def get(self):\n if self.login_required and not self.session.get(\"logged_in\"):\n return web.HTTPFound(\"/\")\n- channel = await self.services.channel.get(uid=self.request.match_info.get(\"channel\"))\n+ channel = await self.services.channel.get(\n+ uid=self.request.match_info.get(\"channel\")\n+ )\n if not channel:\n- user = await self.services.user.get(uid=self.request.match_info.get(\"channel\"))\n+ user = await self.services.user.get(\n+ uid=self.request.match_info.get(\"channel\")\n+ )\n if user:\n- channel = await self.services.channel.get_dm(self.session.get(\"uid\"), user[\"uid\"])\n+ channel = await self.services.channel.get_dm(\n+ self.session.get(\"uid\"), user[\"uid\"]\n+ )\n if channel:\n return web.HTTPFound(\"/channel/{}.html\".format(channel[\"uid\"]))\n if not channel:\n return web.HTTPNotFound()\n- \n- channel_member = await self.app.services.channel_member.get(user_uid=self.session.get(\"uid\"), channel_uid=channel[\"uid\"])\n+\n+ channel_member = await self.app.services.channel_member.get(\n+ user_uid=self.session.get(\"uid\"), channel_uid=channel[\"uid\"]\n+ )\n if not channel_member:\n return web.HTTPNotFound()\n- \n- channel_member['new_count'] = 0\n+\n+ channel_member[\"new_count\"] = 0\n await self.app.services.channel_member.save(channel_member)\n \n user = await self.services.user.get(uid=self.session.get(\"uid\"))\n- messages = [await self.app.services.channel_message.to_extended_dict(message) for message in await self.app.services.channel_message.offset(\n- channel[\"uid\"]\n- )]\n+ messages = [\n+ await self.app.services.channel_message.to_extended_dict(message)\n+ for message in await self.app.services.channel_message.offset(\n+ channel[\"uid\"]\n+ )\n+ ]\n for message in messages:\n- await self.app.services.notification.mark_as_read(self.session.get(\"uid\"),message[\"uid\"])\n+ await self.app.services.notification.mark_as_read(\n+ self.session.get(\"uid\"), message[\"uid\"]\n+ )\n \n name = await channel_member.get_name()\n- return await self.render_template(\"web.html\", {\"name\": name, \"channel\": channel,\"user\": user,\"messages\": messages})\n+ return await self.render_template(\n+ \"web.html\",\n+ {\"name\": name, \"channel\": channel, \"user\": user, \"messages\": messages},\n+ )\ndiff --git a/src/snekssh/app.py b/src/snekssh/app.py\nindex 27e1b4b..aab17e4 100644\n--- a/src/snekssh/app.py\n+++ b/src/snekssh/app.py\n@@ -1,7 +1,9 @@\n import asyncio\n-import asyncssh\n-import os\n import logging\n+import os\n+\n+import asyncssh\n+\n asyncssh.set_debug_level(2)\n logging.basicConfig(level=logging.DEBUG)\n@@ -11,6 +13,7 @@ PASSWORD = \"woeii\"\n HOST = \"localhost\"\n PORT = 2225\n \n+\n class MySFTPServer(asyncssh.SFTPServer):\n def __init__(self, chan):\n super().__init__(chan)\n@@ -31,8 +34,10 @@ class MySFTPServer(asyncssh.SFTPServer):\n full_path = os.path.join(self.root, path.lstrip(\"/\"))\n return await super().listdir(full_path)\n \n+\n class MySSHServer(asyncssh.SSHServer):\n \"\"\"Custom SSH server to handle authentication\"\"\"\n+\n def connection_made(self, conn):\n print(f\"New connection from {conn.get_extra_info('peername')}\")\n \n@@ -46,11 +51,12 @@ class MySSHServer(asyncssh.SSHServer):\n \n def validate_password(self, username, password):\n- print(username,password)\n- \n+ print(username, password)\n+\n return True\n return username == USERNAME and password == PASSWORD\n \n+\n async def start_sftp_server():\n \n@@ -59,11 +65,12 @@ async def start_sftp_server():\n host=HOST,\n port=PORT,\n server_host_keys=[\"ssh_host_key\"],\n- process_factory=MySFTPServer\n+ process_factory=MySFTPServer,\n )\n print(f\"SFTP server running on {HOST}:{PORT}\")\n \n+\n if __name__ == \"__main__\":\n try:\n asyncio.run(start_sftp_server())\ndiff --git a/src/snekssh/app2.py b/src/snekssh/app2.py\nindex 1bfec21..2fa26a7 100644\n--- a/src/snekssh/app2.py\n+++ b/src/snekssh/app2.py\n@@ -1,7 +1,8 @@\n import asyncio\n-import asyncssh\n import os\n \n+import asyncssh\n+\n HOST = \"0.0.0.0\"\n PORT = 2225\n@@ -9,6 +10,7 @@ USERNAME = \"user\"\n PASSWORD = \"password\"\n \n+\n class CustomSSHServer(asyncssh.SSHServer):\n def connection_made(self, conn):\n print(f\"New connection from {conn.get_extra_info('peername')}\")\n@@ -22,6 +24,7 @@ class CustomSSHServer(asyncssh.SSHServer):\n def validate_password(self, username, password):\n return username == USERNAME and password == PASSWORD\n \n+\n async def custom_bash_process(process):\n \"\"\"Spawns a custom bash shell process\"\"\"\n env = os.environ.copy()\n@@ -29,7 +32,12 @@ async def custom_bash_process(process):\n \n bash_proc = await asyncio.create_subprocess_exec(\n- SHELL, \"-i\", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=env\n+ SHELL,\n+ \"-i\",\n+ stdin=asyncio.subprocess.PIPE,\n+ stdout=asyncio.subprocess.PIPE,\n+ stderr=asyncio.subprocess.PIPE,\n+ env=env,\n )\n \n async def read_output():\n@@ -48,6 +56,7 @@ async def custom_bash_process(process):\n \n await asyncio.gather(read_output(), read_input())\n \n+\n async def start_ssh_server():\n \"\"\"Starts the AsyncSSH server with Bash\"\"\"\n await asyncssh.create_server(\n@@ -55,14 +64,14 @@ async def start_ssh_server():\n host=HOST,\n port=PORT,\n server_host_keys=[\"ssh_host_key\"],\n- process_factory=custom_bash_process\n+ process_factory=custom_bash_process,\n )\n print(f\"SSH server running on {HOST}:{PORT}\")\n \n+\n if __name__ == \"__main__\":\n try:\n asyncio.run(start_ssh_server())\n except (OSError, asyncssh.Error) as e:\n print(f\"Error starting SSH server: {e}\")\n-\ndiff --git a/src/snekssh/app3.py b/src/snekssh/app3.py\nindex d50cc54..4a09452 100644\n--- a/src/snekssh/app3.py\n+++ b/src/snekssh/app3.py\n@@ -27,45 +27,48 @@\n \n-import asyncio, asyncssh, sys\n-\n-async def handle_client(process: asyncssh.SSHServerProcess) -> None:\n-\n+import asyncio\n+import sys\n \n+import asyncssh\n \n \n+async def handle_client(process: asyncssh.SSHServerProcess) -> None:\n \n width, height, pixwidth, pixheight = process.term_size\n \n- process.stdout.write(f'Terminal type: {process.term_type}, '\n- f'size: {width}x{height}')\n+ process.stdout.write(\n+ f\"Terminal type: {process.term_type}, \" f\"size: {width}x{height}\"\n+ )\n if pixwidth and pixheight:\n- process.stdout.write(f' ({pixwidth}x{pixheight} pixels)')\n- process.stdout.write('\\nTry resizing your window!\\n')\n+ process.stdout.write(f\" ({pixwidth}x{pixheight} pixels)\")\n+ process.stdout.write(\"\\nTry resizing your window!\\n\")\n \n while not process.stdin.at_eof():\n try:\n await process.stdin.read()\n except asyncssh.TerminalSizeChanged as exc:\n- process.stdout.write(f'New window size: {exc.width}x{exc.height}')\n+ process.stdout.write(f\"New window size: {exc.width}x{exc.height}\")\n if exc.pixwidth and exc.pixheight:\n- process.stdout.write(f' ({exc.pixwidth}'\n- f'x{exc.pixheight} pixels)')\n- process.stdout.write('\\n')\n-\n-\n+ process.stdout.write(f\" ({exc.pixwidth}\" f\"x{exc.pixheight} pixels)\")\n+ process.stdout.write(\"\\n\")\n \n \n async def start_server() -> None:\n- await asyncssh.listen('', 2230, server_host_keys=['ssh_host_key'],\n- process_factory=handle_client)\n+ await asyncssh.listen(\n+ \"\",\n+ 2230,\n+ server_host_keys=[\"ssh_host_key\"],\n+ process_factory=handle_client,\n+ )\n+\n \n loop = asyncio.new_event_loop()\n \n try:\n loop.run_until_complete(start_server())\n except (OSError, asyncssh.Error) as exc:\n- sys.exit('Error starting server: ' + str(exc))\n+ sys.exit(\"Error starting server: \" + str(exc))\n \n loop.run_forever()\ndiff --git a/src/snekssh/app4.py b/src/snekssh/app4.py\nindex e54480f..187722c 100644\n--- a/src/snekssh/app4.py\n+++ b/src/snekssh/app4.py\n@@ -24,32 +24,39 @@\n \n-import asyncio, asyncssh, bcrypt, sys\n+import asyncio\n+import sys\n from typing import Optional\n \n- 'user': bcrypt.hashpw(b'user', bcrypt.gensalt()),\n- }\n+import asyncssh\n+import bcrypt\n+\n+passwords = {\n+ \"user\": bcrypt.hashpw(b\"user\", bcrypt.gensalt()),\n+}\n+\n \n def handle_client(process: asyncssh.SSHServerProcess) -> None:\n- username = process.get_extra_info('username')\n- process.stdout.write(f'Welcome to my SSH server, {username}!\\n')\n+ username = process.get_extra_info(\"username\")\n+ process.stdout.write(f\"Welcome to my SSH server, {username}!\\n\")\n+\n \n class MySSHServer(asyncssh.SSHServer):\n def connection_made(self, conn: asyncssh.SSHServerConnection) -> None:\n- peername = conn.get_extra_info('peername')[0]\n- print(f'SSH connection received from {peername}.')\n+ peername = conn.get_extra_info(\"peername\")[0]\n+ print(f\"SSH connection received from {peername}.\")\n \n def connection_lost(self, exc: Optional[Exception]) -> None:\n if exc:\n- print('SSH connection error: ' + str(exc), file=sys.stderr)\n+ print(\"SSH connection error: \" + str(exc), file=sys.stderr)\n else:\n- print('SSH connection closed.')\n+ print(\"SSH connection closed.\")\n \n def begin_auth(self, username: str) -> bool:\n- return passwords.get(username) != b''\n+ return passwords.get(username) != b\"\"\n \n def password_auth_supported(self) -> bool:\n return True\n@@ -60,18 +67,24 @@ class MySSHServer(asyncssh.SSHServer):\n pw = passwords[username]\n if not password and not pw:\n return True\n- return bcrypt.checkpw(password.encode('utf-8'), pw)\n+ return bcrypt.checkpw(password.encode(\"utf-8\"), pw)\n+\n \n async def start_server() -> None:\n- await asyncssh.create_server(MySSHServer, '', 2231,\n- server_host_keys=['ssh_host_key'],\n- process_factory=handle_client)\n+ await asyncssh.create_server(\n+ MySSHServer,\n+ \"\",\n+ 2231,\n+ server_host_keys=[\"ssh_host_key\"],\n+ process_factory=handle_client,\n+ )\n+\n \n loop = asyncio.new_event_loop()\n \n try:\n loop.run_until_complete(start_server())\n except (OSError, asyncssh.Error) as exc:\n- sys.exit('Error starting server: ' + str(exc))\n+ sys.exit(\"Error starting server: \" + str(exc))\n \n loop.run_forever()\ndiff --git a/src/snekssh/app5.py b/src/snekssh/app5.py\nindex bbaf3d3..cfd5d21 100644\n--- a/src/snekssh/app5.py\n+++ b/src/snekssh/app5.py\n@@ -27,11 +27,15 @@\n \n-import asyncio, asyncssh, sys\n+import asyncio\n+import sys\n from typing import List, cast\n \n+import asyncssh\n+\n+\n class ChatClient:\n- _clients: List['ChatClient'] = []\n+ _clients: List[\"ChatClient\"] = []\n \n def __init__(self, process: asyncssh.SSHServerProcess):\n self._process = process\n@@ -40,8 +44,6 @@ class ChatClient:\n async def handle_client(cls, process: asyncssh.SSHServerProcess):\n await cls(process).run()\n \n- \n-\n async def readline(self) -> str:\n return cast(str, self._process.stdin.readline())\n \n@@ -53,54 +55,58 @@ class ChatClient:\n if client != self:\n client.write(msg)\n \n-\n def begin_auth(self, username: str) -> bool:\n \n def password_auth_supported(self) -> bool:\n return True\n \n def validate_password(self, username: str, password: str) -> bool:\n return True\n-\n \n async def run(self) -> None:\n- self.write('Welcome to chat!\\n\\n')\n+ self.write(\"Welcome to chat!\\n\\n\")\n \n- self.write('Enter your name: ')\n- name = (await self.readline()).rstrip('\\n')\n+ self.write(\"Enter your name: \")\n+ name = (await self.readline()).rstrip(\"\\n\")\n \n- self.write(f'\\n{len(self._clients)} other users are connected.\\n\\n')\n+ self.write(f\"\\n{len(self._clients)} other users are connected.\\n\\n\")\n \n self._clients.append(self)\n- self.broadcast(f'*** {name} has entered chat ***\\n')\n+ self.broadcast(f\"*** {name} has entered chat ***\\n\")\n \n try:\n async for line in self._process.stdin:\n- self.broadcast(f'{name}: {line}')\n+ self.broadcast(f\"{name}: {line}\")\n except asyncssh.BreakReceived:\n pass\n \n- self.broadcast(f'*** {name} has left chat ***\\n')\n+ self.broadcast(f\"*** {name} has left chat ***\\n\")\n self._clients.remove(self)\n \n+\n async def start_server() -> None:\n- await asyncssh.listen('', 2235, server_host_keys=['ssh_host_key'],\n- process_factory=ChatClient.handle_client)\n+ await asyncssh.listen(\n+ \"\",\n+ 2235,\n+ server_host_keys=[\"ssh_host_key\"],\n+ process_factory=ChatClient.handle_client,\n+ )\n+\n \n loop = asyncio.new_event_loop()\n \n try:\n loop.run_until_complete(start_server())\n except (OSError, asyncssh.Error) as exc:\n- sys.exit('Error starting server: ' + str(exc))\n+ sys.exit(\"Error starting server: \" + str(exc))\n \n loop.run_forever()"} +{"repo": ".", "date": "2025-03-29", "line": "feat: Initial implementation of WebDAV functionality with basic operations", "commit": "9a923f6bddd73df27af80ef6c8e2313816a07a48", "diff": "commit 9a923f6bddd73df27af80ef6c8e2313816a07a48\nAuthor: retoor \nDate: Sat Mar 29 07:15:53 2025 +0100\n\n WEebdav.\n\ndiff --git a/src/snek/__init__.py b/src/snek/__init__.py\nnew file mode 100644\nindex 0000000..b28b04f\n--- /dev/null\n+++ b/src/snek/__init__.py\n@@ -0,0 +1,3 @@\n+\n+\n+\ndiff --git a/src/snek/__main__.py b/src/snek/__main__.py\nnew file mode 100644\nindex 0000000..30f0209\n--- /dev/null\n+++ b/src/snek/__main__.py\n@@ -0,0 +1,5 @@\n+from aiohttp import web \n+from snek.app import Application\n+\n+if __name__ == '__main__':\n+ web.run_app(Application(), port=8081,host='0.0.0.0')\ndiff --git a/src/snek/webdav.py b/src/snek/webdav.py\nnew file mode 100755\nindex 0000000..66aeb3d\n--- /dev/null\n+++ b/src/snek/webdav.py\n@@ -0,0 +1,348 @@\n+import logging\n+\n+import pathlib\n+logging.basicConfig(level=logging.DEBUG)\n+\n+import asyncio\n+import base64\n+import datetime\n+import mimetypes\n+import os\n+import shutil\n+import uuid\n+\n+import aiofiles\n+import aiohttp\n+import aiohttp.web\n+from lxml import etree\n+\n+\n+class WebdavApplication(aiohttp.web.Application):\n+ def __init__(self, parent, *args, **kwargs):\n+ super().__init__(*args, **kwargs)\n+ self.locks = {}\n+ \n+ self.router.add_route(\"OPTIONS\", \"/{filename:.*}\", self.handle_options)\n+ self.router.add_route(\"GET\", \"/{filename:.*}\", self.handle_get)\n+ self.router.add_route(\"PUT\", \"/{filename:.*}\", self.handle_put)\n+ self.router.add_route(\"DELETE\", \"/{filename:.*}\", self.handle_delete)\n+ self.router.add_route(\"MKCOL\", \"/{filename:.*}\", self.handle_mkcol)\n+ self.router.add_route(\"MOVE\", \"/{filename:.*}\", self.handle_move)\n+ self.router.add_route(\"COPY\", \"/{filename:.*}\", self.handle_copy)\n+ self.router.add_route(\"PROPFIND\", \"/{filename:.*}\", self.handle_propfind)\n+ self.router.add_route(\"PROPPATCH\", \"/{filename:.*}\", self.handle_proppatch)\n+ self.router.add_route(\"LOCK\", \"/{filename:.*}\", self.handle_lock)\n+ self.router.add_route(\"UNLOCK\", \"/{filename:.*}\", self.handle_unlock)\n+ self.parent = parent\n+\n+ @property \n+ def db(self):\n+ return self.parent.db\n+\n+ @property \n+ def services(self):\n+ return self.parent.services \n+\n+\n+ async def authenticate(self, request):\n+ session = request.session\n+ if session.get('uid'):\n+ request['user'] = await self.services.user.get(uid=session['uid'])\n+ try:\n+ request['home'] = await self.services.user.get_home_folder(user_uid=request['user']['uid'])\n+ except:\n+ pass\n+ return user\n+\n+ auth_header = request.headers.get(\"Authorization\", \"\")\n+ if not auth_header.startswith(\"Basic \"):\n+ return False\n+ encoded_creds = auth_header.split(\"Basic \")[1]\n+ decoded_creds = base64.b64decode(encoded_creds).decode()\n+ username, password = decoded_creds.split(\":\", 1)\n+ request['user'] = await self.services.user.authenticate(username=username, password=password)\n+ try:\n+ request['home'] = await self.services.user.get_home_folder(request['user']['uid'])\n+ except Exception as ex:\n+ print(ex)\n+ pass\n+ return request['user']\n+\n+ async def handle_get(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+\n+ requested_path = request.match_info.get(\"filename\", \"\")\n+ abs_path = request['home'] / requested_path\n+\n+ if not abs_path.exists():\n+ return aiohttp.web.Response(status=404, text=\"File not found\")\n+\n+ if abs_path.is_dir():\n+ return aiohttp.web.Response(status=403, text=\"Cannot download a directory\")\n+\n+ content_type, _ = mimetypes.guess_type(str(abs_path))\n+ content_type = content_type or \"application/octet-stream\"\n+\n+ return aiohttp.web.FileResponse(\n+ path=str(abs_path), headers={\"Content-Type\": content_type}, chunk_size=8192\n+ )\n+\n+ async def handle_put(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ file_path = request['home'] / request.match_info[\"filename\"]\n+ file_path.parent.mkdir(parents=True, exist_ok=True)\n+ async with aiofiles.open(file_path, \"wb\") as f:\n+ while chunk := await request.content.read(1024):\n+ await f.write(chunk)\n+ return aiohttp.web.Response(status=201, text=\"File uploaded\")\n+\n+ async def handle_delete(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ file_path = request['home'] / request.match_info[\"filename\"]\n+ if file_path.is_file():\n+ file_path.unlink()\n+ return aiohttp.web.Response(status=204)\n+ elif file_path.is_dir():\n+ shutil.rmtree(file_path)\n+ return aiohttp.web.Response(status=204)\n+ return aiohttp.web.Response(status=404, text=\"Not found\")\n+\n+ async def handle_mkcol(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ dir_path = request['home'] / request.match_info[\"filename\"]\n+ if dir_path.exists():\n+ return aiohttp.web.Response(status=405, text=\"Directory already exists\")\n+ dir_path.mkdir(parents=True, exist_ok=True)\n+ return aiohttp.web.Response(status=201, text=\"Directory created\")\n+\n+ async def handle_move(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ src_path = request['home'] / request.match_info[\"filename\"]\n+ dest_path = request['home'] / request.headers.get(\"Destination\", \"\").replace(\n+ )\n+ if not src_path.exists():\n+ return aiohttp.web.Response(status=404, text=\"Source not found\")\n+ shutil.move(str(src_path), str(dest_path))\n+ return aiohttp.web.Response(status=201, text=\"Moved successfully\")\n+\n+ async def handle_copy(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ src_path = request['home'] / request.match_info[\"filename\"]\n+ dest_path = request['home'] / request.headers.get(\"Destination\", \"\").replace(\n+ )\n+ if not src_path.exists():\n+ return aiohttp.web.Response(status=404, text=\"Source not found\")\n+ if src_path.is_file():\n+ shutil.copy2(str(src_path), str(dest_path))\n+ else:\n+ shutil.copytree(str(src_path), str(dest_path))\n+ return aiohttp.web.Response(status=201, text=\"Copied successfully\")\n+\n+ async def handle_options(self, request):\n+ headers = {\n+ \"DAV\": \"1, 2\",\n+ \"Allow\": \"OPTIONS, GET, PUT, DELETE, MKCOL, MOVE, COPY, PROPFIND, PROPPATCH\",\n+ }\n+ print(\"RETURN\")\n+ return aiohttp.web.Response(status=200, headers=headers)\n+\n+ def get_current_utc_time(self, filepath):\n+ if filepath.exists():\n+ modified_time = datetime.datetime.utcfromtimestamp(filepath.stat().st_mtime)\n+ else:\n+ modified_time = datetime.datetime.utcnow()\n+ return modified_time.strftime(\"%Y-%m-%dT%H:%M:%SZ\"), modified_time.strftime(\n+ \"%a, %d %b %Y %H:%M:%S GMT\"\n+ )\n+\n+ def get_directory_size(self, directory):\n+ total_size = 0\n+ for dirpath, _, filenames in os.walk(directory):\n+ for f in filenames:\n+ fp = pathlib.Path(dirpath) / f\n+ if fp.exists():\n+ total_size += fp.stat().st_size\n+ return total_size\n+\n+ def get_disk_free_space(self, path):\n+ statvfs = os.statvfs(path)\n+ return statvfs.f_bavail * statvfs.f_frsize\n+\n+ async def handle_propfind(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ requested_path = request.match_info.get(\"filename\", \"\") \n+ abs_path = request['home'] / requested_path\n+ if not abs_path.exists():\n+ return aiohttp.web.Response(status=404, text=\"Directory not found\")\n+ nsmap = {\"D\": \"DAV:\"}\n+ response_xml = etree.Element(\"{DAV:}multistatus\", nsmap=nsmap)\n+ directories = [requested_path]\n+ if abs_path.is_dir():\n+ directories.extend(os.listdir(abs_path))\n+ for item in directories:\n+ full_path = abs_path / item if item != requested_path else abs_path\n+ href_path = f\"/{requested_path}/{item}/\" if item != requested_path else f\"/{requested_path}/\"\n+ response = etree.SubElement(response_xml, \"{DAV:}response\")\n+ href = etree.SubElement(response, \"{DAV:}href\")\n+ if not full_path.is_dir():\n+ href_path = href_path.rstrip(\"/\")\n+ href.text = href_path\n+ propstat = etree.SubElement(response, \"{DAV:}propstat\")\n+ prop = etree.SubElement(propstat, \"{DAV:}prop\")\n+ res_type = etree.SubElement(prop, \"{DAV:}resourcetype\")\n+ if full_path.is_dir():\n+ etree.SubElement(res_type, \"{DAV:}collection\")\n+ creation_date, last_modified = self.get_current_utc_time(full_path)\n+ etree.SubElement(prop, \"{DAV:}creationdate\").text = creation_date\n+ etree.SubElement(prop, \"{DAV:}quota-used-bytes\").text = str(\n+ full_path.stat().st_size\n+ if full_path.is_file()\n+ else self.get_directory_size(full_path)\n+ )\n+ etree.SubElement(prop, \"{DAV:}quota-available-bytes\").text = str(\n+ self.get_disk_free_space(request['home'])\n+ )\n+ etree.SubElement(prop, \"{DAV:}getlastmodified\").text = last_modified\n+ etree.SubElement(prop, \"{DAV:}displayname\").text = full_path.name\n+ etree.SubElement(prop, \"{DAV:}lockdiscovery\")\n+ mimetype, _ = mimetypes.guess_type(full_path.name)\n+ etree.SubElement(prop, \"{DAV:}contenttype\").text = mimetype\n+ etree.SubElement(prop, \"{DAV:}getcontentlength\").text = str(\n+ full_path.stat().st_size\n+ if full_path.is_file()\n+ else self.get_directory_size(full_path)\n+ )\n+ supported_lock = etree.SubElement(prop, \"{DAV:}supportedlock\")\n+ lock_entry_1 = etree.SubElement(supported_lock, \"{DAV:}lockentry\")\n+ lock_scope_1 = etree.SubElement(lock_entry_1, \"{DAV:}lockscope\")\n+ etree.SubElement(lock_scope_1, \"{DAV:}exclusive\")\n+ lock_type_1 = etree.SubElement(lock_entry_1, \"{DAV:}locktype\")\n+ etree.SubElement(lock_type_1, \"{DAV:}write\")\n+ lock_entry_2 = etree.SubElement(supported_lock, \"{DAV:}lockentry\")\n+ lock_scope_2 = etree.SubElement(lock_entry_2, \"{DAV:}lockscope\")\n+ etree.SubElement(lock_scope_2, \"{DAV:}shared\")\n+ lock_type_2 = etree.SubElement(lock_entry_2, \"{DAV:}locktype\")\n+ etree.SubElement(lock_type_2, \"{DAV:}write\")\n+ etree.SubElement(propstat, \"{DAV:}status\").text = \"HTTP/1.1 200 OK\"\n+ xml_output = etree.tostring(\n+ response_xml, encoding=\"utf-8\", xml_declaration=True\n+ ).decode()\n+ return aiohttp.web.Response(\n+ status=207, text=xml_output, content_type=\"application/xml\"\n+ )\n+\n+ async def handle_proppatch(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ return aiohttp.web.Response(status=207, text=\"PROPPATCH OK (Not Implemented)\")\n+\n+ async def handle_lock(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ resource = request.match_info.get(\"filename\", \"/\")\n+ lock_id = str(uuid.uuid4())\n+ self.locks[resource] = lock_id\n+ xml_response = self.generate_lock_response(lock_id)\n+ headers = {\n+ \"Lock-Token\": f\"opaquelocktoken:{lock_id}\",\n+ \"Content-Type\": \"application/xml\",\n+ }\n+ return aiohttp.web.Response(text=xml_response, headers=headers, status=200)\n+\n+ async def handle_unlock(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+ resource = request.match_info.get(\"filename\", \"/\")\n+ lock_token = request.headers.get(\"Lock-Token\", \"\").replace(\n+ \"opaquelocktoken:\", \"\"\n+ )\n+ if self.locks.get(resource) == lock_token:\n+ del self.locks[resource]\n+ return aiohttp.web.Response(status=204)\n+ return aiohttp.web.Response(status=400, text=\"Invalid Lock Token\")\n+\n+ def generate_lock_response(self, lock_id):\n+ nsmap = {\"D\": \"DAV:\"}\n+ root = etree.Element(\"{DAV:}prop\", nsmap=nsmap)\n+ lock_discovery = etree.SubElement(root, \"{DAV:}lockdiscovery\")\n+ active_lock = etree.SubElement(lock_discovery, \"{DAV:}activelock\")\n+ lock_type = etree.SubElement(active_lock, \"{DAV:}locktype\")\n+ etree.SubElement(lock_type, \"{DAV:}write\")\n+ lock_scope = etree.SubElement(active_lock, \"{DAV:}lockscope\")\n+ etree.SubElement(lock_scope, \"{DAV:}exclusive\")\n+ etree.SubElement(active_lock, \"{DAV:}depth\").text = \"Infinity\"\n+ owner = etree.SubElement(active_lock, \"{DAV:}owner\")\n+ etree.SubElement(owner, \"{DAV:}href\").text = lock_id\n+ etree.SubElement(active_lock, \"{DAV:}timeout\").text = \"Infinite\"\n+ lock_token = etree.SubElement(active_lock, \"{DAV:}locktoken\")\n+ etree.SubElement(lock_token, \"{DAV:}href\").text = f\"opaquelocktoken:{lock_id}\"\n+ return etree.tostring(root, pretty_print=True, encoding=\"utf-8\").decode()\n+\n+ def get_last_modified(self, path):\n+ if not path.exists():\n+ return None\n+ timestamp = path.stat().st_mtime\n+ dt = datetime.datetime.utcfromtimestamp(timestamp)\n+ return dt.strftime(\"%a, %d %b %Y %H:%M:%S GMT\")\n+\n+ async def handle_head(self, request):\n+ if not await self.authenticate(request):\n+ return aiohttp.web.Response(\n+ status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n+ )\n+\n+ requested_path = request.match_info.get(\"filename\", \"\")\n+ print(requested_path) \n+ abs_path = request['home'] / requested_path\n+\n+ if not abs_path.exists():\n+ return aiohttp.web.Response(status=404, text=\"File not found\")\n+\n+ if abs_path.is_dir():\n+ return aiohttp.web.Response(\n+ status=403, text=\"Cannot get metadata for a directory\"\n+ )\n+\n+ content_type, _ = mimetypes.guess_type(str(abs_path))\n+ content_type = content_type or \"application/octet-stream\"\n+ file_size = abs_path.stat().st_size\n+\n+ headers = {\n+ \"Content-Type\": content_type,\n+ \"Content-Length\": str(file_size),\n+ \"Last-Modified\": self.get_last_modified(abs_path),\n+ }\n+\n+ return aiohttp.web.Response(status=200, headers=headers)\n+\n+"} +{"repo": ".", "date": "2025-03-29", "line": "feat: Implemented basic WebDAV authentication", "commit": "886d21999c716ee306318796f3f159ba085f9618", "diff": "commit 886d21999c716ee306318796f3f159ba085f9618\nAuthor: retoor \nDate: Sat Mar 29 07:19:08 2025 +0100\n\n Added webdav.\n\ndiff --git a/src/snek/webdav.py b/src/snek/webdav.py\nindex 66aeb3d..3033e3a 100755\n--- a/src/snek/webdav.py\n+++ b/src/snek/webdav.py\n@@ -45,14 +45,14 @@ class WebdavApplication(aiohttp.web.Application):\n \n \n async def authenticate(self, request):\n- session = request.session\n- if session.get('uid'):\n- request['user'] = await self.services.user.get(uid=session['uid'])\n- try:\n- request['home'] = await self.services.user.get_home_folder(user_uid=request['user']['uid'])\n- except:\n- pass\n- return user\n \n auth_header = request.headers.get(\"Authorization\", \"\")\n if not auth_header.startswith(\"Basic \"):"} +{"repo": ".", "date": "2025-03-29", "line": "feat: Use `/home` instead of `/drive` for user home folders", "commit": "6dca3a96e1cd23cc26d69aeee31ae45e14977bbd", "diff": "commit 6dca3a96e1cd23cc26d69aeee31ae45e14977bbd\nAuthor: retoor \nDate: Sat Mar 29 07:27:56 2025 +0100\n\n Added webdav.\n\ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nindex 6055403..70e8adf 100644\n--- a/src/snek/service/user.py\n+++ b/src/snek/service/user.py\n@@ -40,7 +40,7 @@ class UserService(BaseService):\n return model\n \n async def get_home_folder(self, user_uid):\n- folder = pathlib.Path(f\"./drive/{user_uid}\")\n+ folder = pathlib.Path(f\"./home/{user_uid}\")\n if not folder.exists():\n folder.mkdir(parents=True, exist_ok=True)\n return folder"} +{"repo": ".", "date": "2025-03-29", "line": "feat: Implemented recursive node creation for propfind requests", "commit": "3926b2d837bef181546d826b41821cf90fc755a8", "diff": "commit 3926b2d837bef181546d826b41821cf90fc755a8\nAuthor: retoor \nDate: Sat Mar 29 10:50:40 2025 +0100\n\n Update storage.\n\ndiff --git a/src/snek/webdav.py b/src/snek/webdav.py\nindex 3033e3a..e03e675 100755\n--- a/src/snek/webdav.py\n+++ b/src/snek/webdav.py\n@@ -188,11 +188,74 @@ class WebdavApplication(aiohttp.web.Application):\n statvfs = os.statvfs(path)\n return statvfs.f_bavail * statvfs.f_frsize\n \n+\n+ async def create_node(self, request, response_xml, full_path, depth):\n+ requested_path = request.match_info.get(\"filename\", \"\") \n+ abs_path = pathlib.Path(full_path)\n+ relative_path = str(full_path.relative_to(request['home']))\n+\n+ href_path = f\"{relative_path}\".strip(\"/\")\n+ response = etree.SubElement(response_xml, \"{DAV:}response\")\n+ href = etree.SubElement(response, \"{DAV:}href\")\n+ href.text = href_path\n+ propstat = etree.SubElement(response, \"{DAV:}propstat\")\n+ prop = etree.SubElement(propstat, \"{DAV:}prop\")\n+ res_type = etree.SubElement(prop, \"{DAV:}resourcetype\")\n+ if full_path.is_dir():\n+ etree.SubElement(res_type, \"{DAV:}collection\")\n+ creation_date, last_modified = self.get_current_utc_time(full_path)\n+ etree.SubElement(prop, \"{DAV:}creationdate\").text = creation_date\n+ etree.SubElement(prop, \"{DAV:}quota-used-bytes\").text = str(\n+ full_path.stat().st_size\n+ if full_path.is_file()\n+ else self.get_directory_size(full_path)\n+ )\n+ etree.SubElement(prop, \"{DAV:}quota-available-bytes\").text = str(\n+ self.get_disk_free_space(request['home'])\n+ )\n+ etree.SubElement(prop, \"{DAV:}getlastmodified\").text = last_modified\n+ etree.SubElement(prop, \"{DAV:}displayname\").text = full_path.name\n+ etree.SubElement(prop, \"{DAV:}lockdiscovery\")\n+ mimetype, _ = mimetypes.guess_type(full_path.name)\n+ etree.SubElement(prop, \"{DAV:}contenttype\").text = mimetype\n+ etree.SubElement(prop, \"{DAV:}getcontentlength\").text = str(\n+ full_path.stat().st_size\n+ if full_path.is_file()\n+ else self.get_directory_size(full_path)\n+ )\n+ supported_lock = etree.SubElement(prop, \"{DAV:}supportedlock\")\n+ lock_entry_1 = etree.SubElement(supported_lock, \"{DAV:}lockentry\")\n+ lock_scope_1 = etree.SubElement(lock_entry_1, \"{DAV:}lockscope\")\n+ etree.SubElement(lock_scope_1, \"{DAV:}exclusive\")\n+ lock_type_1 = etree.SubElement(lock_entry_1, \"{DAV:}locktype\")\n+ etree.SubElement(lock_type_1, \"{DAV:}write\")\n+ lock_entry_2 = etree.SubElement(supported_lock, \"{DAV:}lockentry\")\n+ lock_scope_2 = etree.SubElement(lock_entry_2, \"{DAV:}lockscope\")\n+ etree.SubElement(lock_scope_2, \"{DAV:}shared\")\n+ lock_type_2 = etree.SubElement(lock_entry_2, \"{DAV:}locktype\")\n+ etree.SubElement(lock_type_2, \"{DAV:}write\")\n+ etree.SubElement(propstat, \"{DAV:}status\").text = \"HTTP/1.1 200 OK\"\n+ \n+ if abs_path.is_dir() and depth != -1:\n+ for item in abs_path.iterdir():\n+ await self.create_node(request,response_xml, item, depth - 1)\n+\n+\n+\n+\n async def handle_propfind(self, request):\n if not await self.authenticate(request):\n return aiohttp.web.Response(\n status=401, headers={\"WWW-Authenticate\": 'Basic realm=\"WebDAV\"'}\n )\n+\n+ depth = 0\n+ try:\n+ depth = int(request.headers.get(\"Depth\", \"0\"))\n+ except ValueError:\n+ pass\n requested_path = request.match_info.get(\"filename\", \"\") \n abs_path = request['home'] / requested_path\n if not abs_path.exists():\n@@ -200,54 +263,9 @@ class WebdavApplication(aiohttp.web.Application):\n nsmap = {\"D\": \"DAV:\"}\n response_xml = etree.Element(\"{DAV:}multistatus\", nsmap=nsmap)\n directories = [requested_path]\n- if abs_path.is_dir():\n- directories.extend(os.listdir(abs_path))\n- for item in directories:\n- full_path = abs_path / item if item != requested_path else abs_path\n- href_path = f\"/{requested_path}/{item}/\" if item != requested_path else f\"/{requested_path}/\"\n- response = etree.SubElement(response_xml, \"{DAV:}response\")\n- href = etree.SubElement(response, \"{DAV:}href\")\n- if not full_path.is_dir():\n- href_path = href_path.rstrip(\"/\")\n- href.text = href_path\n- propstat = etree.SubElement(response, \"{DAV:}propstat\")\n- prop = etree.SubElement(propstat, \"{DAV:}prop\")\n- res_type = etree.SubElement(prop, \"{DAV:}resourcetype\")\n- if full_path.is_dir():\n- etree.SubElement(res_type, \"{DAV:}collection\")\n- creation_date, last_modified = self.get_current_utc_time(full_path)\n- etree.SubElement(prop, \"{DAV:}creationdate\").text = creation_date\n- etree.SubElement(prop, \"{DAV:}quota-used-bytes\").text = str(\n- full_path.stat().st_size\n- if full_path.is_file()\n- else self.get_directory_size(full_path)\n- )\n- etree.SubElement(prop, \"{DAV:}quota-available-bytes\").text = str(\n- self.get_disk_free_space(request['home'])\n- )\n- etree.SubElement(prop, \"{DAV:}getlastmodified\").text = last_modified\n- etree.SubElement(prop, \"{DAV:}displayname\").text = full_path.name\n- etree.SubElement(prop, \"{DAV:}lockdiscovery\")\n- mimetype, _ = mimetypes.guess_type(full_path.name)\n- etree.SubElement(prop, \"{DAV:}contenttype\").text = mimetype\n- etree.SubElement(prop, \"{DAV:}getcontentlength\").text = str(\n- full_path.stat().st_size\n- if full_path.is_file()\n- else self.get_directory_size(full_path)\n- )\n- supported_lock = etree.SubElement(prop, \"{DAV:}supportedlock\")\n- lock_entry_1 = etree.SubElement(supported_lock, \"{DAV:}lockentry\")\n- lock_scope_1 = etree.SubElement(lock_entry_1, \"{DAV:}lockscope\")\n- etree.SubElement(lock_scope_1, \"{DAV:}exclusive\")\n- lock_type_1 = etree.SubElement(lock_entry_1, \"{DAV:}locktype\")\n- etree.SubElement(lock_type_1, \"{DAV:}write\")\n- lock_entry_2 = etree.SubElement(supported_lock, \"{DAV:}lockentry\")\n- lock_scope_2 = etree.SubElement(lock_entry_2, \"{DAV:}lockscope\")\n- etree.SubElement(lock_scope_2, \"{DAV:}shared\")\n- lock_type_2 = etree.SubElement(lock_entry_2, \"{DAV:}locktype\")\n- etree.SubElement(lock_type_2, \"{DAV:}write\")\n- etree.SubElement(propstat, \"{DAV:}status\").text = \"HTTP/1.1 200 OK\"\n+ \n+ await self.create_node(request, response_xml, abs_path, depth)\n+\n xml_output = etree.tostring(\n response_xml, encoding=\"utf-8\", xml_declaration=True\n ).decode()"} +{"repo": ".", "date": "2025-03-30", "line": "refactor: Updated home folder path to drive and removed lock implementation", "commit": "d5917b94540aee206935354f438a6a7f893278ec", "diff": "commit d5917b94540aee206935354f438a6a7f893278ec\nAuthor: retoor \nDate: Sun Mar 30 06:54:25 2025 +0200\n\n New version.\n\ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nindex 70e8adf..6055403 100644\n--- a/src/snek/service/user.py\n+++ b/src/snek/service/user.py\n@@ -40,7 +40,7 @@ class UserService(BaseService):\n return model\n \n async def get_home_folder(self, user_uid):\n- folder = pathlib.Path(f\"./home/{user_uid}\")\n+ folder = pathlib.Path(f\"./drive/{user_uid}\")\n if not folder.exists():\n folder.mkdir(parents=True, exist_ok=True)\n return folder\ndiff --git a/src/snek/webdav.py b/src/snek/webdav.py\nindex e03e675..b823c19 100755\n--- a/src/snek/webdav.py\n+++ b/src/snek/webdav.py\n@@ -287,7 +287,7 @@ class WebdavApplication(aiohttp.web.Application):\n )\n resource = request.match_info.get(\"filename\", \"/\")\n lock_id = str(uuid.uuid4())\n- self.locks[resource] = lock_id\n xml_response = self.generate_lock_response(lock_id)\n headers = {\n \"Lock-Token\": f\"opaquelocktoken:{lock_id}\","} +{"repo": ".", "date": "2025-03-30", "line": "refactor: Remove unused LOCK and UNLOCK routes", "commit": "8058e4a4b0aee254edc76fa28b2c4336eb393c4b", "diff": "commit 8058e4a4b0aee254edc76fa28b2c4336eb393c4b\nAuthor: retoor \nDate: Sun Mar 30 07:02:35 2025 +0200\n\n New version.\n\ndiff --git a/src/snek/webdav.py b/src/snek/webdav.py\nindex b823c19..a372b8b 100755\n--- a/src/snek/webdav.py\n+++ b/src/snek/webdav.py\n@@ -31,8 +31,8 @@ class WebdavApplication(aiohttp.web.Application):\n self.router.add_route(\"COPY\", \"/{filename:.*}\", self.handle_copy)\n self.router.add_route(\"PROPFIND\", \"/{filename:.*}\", self.handle_propfind)\n self.router.add_route(\"PROPPATCH\", \"/{filename:.*}\", self.handle_proppatch)\n- self.router.add_route(\"LOCK\", \"/{filename:.*}\", self.handle_lock)\n- self.router.add_route(\"UNLOCK\", \"/{filename:.*}\", self.handle_unlock)\n self.parent = parent\n \n @property"} +{"repo": ".", "date": "2025-03-30", "line": "fix: Handle potential errors when creating home folders", "commit": "2a47c0ba5e7344d44c3acd15d8f3efeb11722677", "diff": "commit 2a47c0ba5e7344d44c3acd15d8f3efeb11722677\nAuthor: retoor \nDate: Sun Mar 30 09:34:13 2025 +0200\n\n Fixed bug.\n\ndiff --git a/src/snek/service/user.py b/src/snek/service/user.py\nindex 6055403..b70be63 100644\n--- a/src/snek/service/user.py\n+++ b/src/snek/service/user.py\n@@ -42,7 +42,10 @@ class UserService(BaseService):\n async def get_home_folder(self, user_uid):\n folder = pathlib.Path(f\"./drive/{user_uid}\")\n if not folder.exists():\n- folder.mkdir(parents=True, exist_ok=True)\n+ try:\n+ folder.mkdir(parents=True, exist_ok=True)\n+ except:\n+ pass\n return folder\n \n async def register(self, email, username, password):"} +{"repo": ".", "date": "2025-04-01", "line": "feat: Added settings page with profile, notifications, and privacy sections", "commit": "32e0c959e8589f574d1c46b96d35f49c98721566", "diff": "commit 32e0c959e8589f574d1c46b96d35f49c98721566\nAuthor: retoor \nDate: Tue Apr 1 16:21:29 2025 +0200\n\n Progress.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex 984fcf3..d043c0e 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -42,6 +42,7 @@ from snek.view.terminal import TerminalSocketView, TerminalView\n from snek.view.upload import UploadView\n from snek.view.web import WebView\n from snek.webdav import WebdavApplication\n+from snek.view.settings import SettingsView\n \n SESSION_KEY = b\"c79a0c5fda4b424189c427d28c9f7c34\"\n \n@@ -137,6 +138,7 @@ class Application(BaseApplication):\n self.router.add_view(\"/docs.html\", DocsHTMLView)\n self.router.add_view(\"/docs.md\", DocsMDView)\n self.router.add_view(\"/status.json\", StatusView)\n+ self.router.add_view(\"/settings.html\", SettingsView)\n self.router.add_view(\"/web.html\", WebView)\n self.router.add_view(\"/login.html\", LoginView)\n self.router.add_view(\"/login.json\", LoginView)\ndiff --git a/src/snek/templates/settings.html b/src/snek/templates/settings.html\nnew file mode 100644\nindex 0000000..cfb186c\n--- /dev/null\n+++ b/src/snek/templates/settings.html\n@@ -0,0 +1,36 @@\n+{% extends \"app.html\" %}\n+\n+{% block sidebar %}\n+\n+{% include \"sidebar_settings.html\" %}\n+\n+{% endblock %}\n+\n+{% block head %}\n+\n+\n+{% endblock %}\n+\n+{% block main %}\n+\n+

Setting page

\n+\n+
\n+\n+\n+\n+\n+\n+{% endblock main %}\ndiff --git a/src/snek/templates/sidebar_settings.html b/src/snek/templates/sidebar_settings.html\nnew file mode 100644\nindex 0000000..8e18412\n--- /dev/null\n+++ b/src/snek/templates/sidebar_settings.html\n@@ -0,0 +1,14 @@\n+\n+\ndiff --git a/src/snek/view/settings.py b/src/snek/view/settings.py\nnew file mode 100644\nindex 0000000..fe181f2\n--- /dev/null\n+++ b/src/snek/view/settings.py\n@@ -0,0 +1,8 @@\n+from snek.system.view import BaseView \n+\n+class SettingsView(BaseView):\n+ \n+ login_required = True\n+\n+ async def get(self):\n+ return await self.render_template('settings.html')\ndiff --git a/src/snek/webdav.py b/src/snek/webdav.py\nindex a372b8b..9a2b9e4 100755\n--- a/src/snek/webdav.py\n+++ b/src/snek/webdav.py\n@@ -219,8 +219,9 @@ class WebdavApplication(aiohttp.web.Application):\n etree.SubElement(prop, \"{DAV:}displayname\").text = full_path.name\n etree.SubElement(prop, \"{DAV:}lockdiscovery\")\n mimetype, _ = mimetypes.guess_type(full_path.name)\n- etree.SubElement(prop, \"{DAV:}contenttype\").text = mimetype\n- etree.SubElement(prop, \"{DAV:}getcontentlength\").text = str(\n+ if full_path.is_file():\n+ etree.SubElement(prop, \"{DAV:}contenttype\").text = mimetype\n+ etree.SubElement(prop, \"{DAV:}getcontentlength\").text = str(\n full_path.stat().st_size\n if full_path.is_file()\n else self.get_directory_size(full_path)"} +{"repo": ".", "date": "2025-04-01", "line": "feat: Improve responsiveness for smaller screens", "commit": "87b48af551d2ed023f77ca57d033ed0079d303f3", "diff": "commit 87b48af551d2ed023f77ca57d033ed0079d303f3\nAuthor: retoor \nDate: Tue Apr 1 20:08:01 2025 +0200\n\n Update css.\n\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex 31a6e74..db9dcb0 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -347,3 +347,15 @@ a {\n }\n \n+@media only screen and (max-width: 600px) {\n+ header{\n+ position: sticky;\n+ display: block;\n+ .logo {\n+ display:block;\n+ }\n+ }\n+ .chat-input {\n+ position:sticky;\n+ }\n+}\ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex 28ad4d2..d386b67 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -12,10 +12,10 @@\n {% endfor %}\n
\n \n-
\n+
\n \n \n-
\n+ \n
\n \n \n \n@@ -14,9 +21,11 @@\n

Snek

\n

Rocket Chat got bloated, too commercialized,\n So Snek came through, lean and optimized.

\n+
\n \n OR\n \n+
\n
\n \n \ndiff --git a/src/snek/view/login.py b/src/snek/view/login.py\nindex be79328..fe8cf4d 100644\n--- a/src/snek/view/login.py\n+++ b/src/snek/view/login.py\n@@ -15,6 +15,8 @@ from snek.system.view import BaseFormView\n class LoginView(BaseFormView):\n form = LoginForm\n \n+ login_required = False\n+\n async def get(self):\n if self.session.get(\"logged_in\"):\n return web.HTTPFound(\"/web.html\")\ndiff --git a/src/snek/view/register.py b/src/snek/view/register.py\nindex 7fbce9d..96eed8a 100644\n--- a/src/snek/view/register.py\n+++ b/src/snek/view/register.py\n@@ -15,6 +15,8 @@ from snek.system.view import BaseFormView\n class RegisterView(BaseFormView):\n form = RegisterForm\n \n+ login_required = False\n+\n async def get(self):\n if self.session.get(\"logged_in\"):\n return web.HTTPFound(\"/web.html\")"} +{"repo": ".", "date": "2025-04-01", "line": "feat: Reduced container width for mobile responsiveness", "commit": "e3c997302b07228c0791d6307b429109b6eb3d53", "diff": "commit e3c997302b07228c0791d6307b429109b6eb3d53\nAuthor: retoor \nDate: Tue Apr 1 21:09:58 2025 +0200\n\n Upated for phone.\n\ndiff --git a/src/snek/static/style.css b/src/snek/static/style.css\nindex f10f780..6e81345 100644\n--- a/src/snek/static/style.css\n+++ b/src/snek/static/style.css\n@@ -13,7 +13,7 @@\n \n border-radius: 10px;\n padding: 30px;\n- width: 600px;\n+ width: 500px;\n margin: 30px;\n box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n }"} +{"repo": ".", "date": "2025-04-01", "line": "feat: Adjusted form width for mobile responsiveness", "commit": "27c0abea3147e5e00a5196fa9b351141a9d17ae2", "diff": "commit 27c0abea3147e5e00a5196fa9b351141a9d17ae2\nAuthor: retoor \nDate: Tue Apr 1 21:23:10 2025 +0200\n\n Upated for phone.\n\ndiff --git a/src/snek/static/generic-form.css b/src/snek/static/generic-form.css\nindex d593816..7010193 100644\n--- a/src/snek/static/generic-form.css\n+++ b/src/snek/static/generic-form.css\n@@ -14,6 +14,7 @@\n justify-content: center;\n align-items: center;\n height: 100vh;\n+ width: 100vw;\n }\n \n generic-form {\n@@ -29,7 +30,6 @@ generic-form {\n border-radius: 10px;\n padding: 30px;\n- width: 400px;\n box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n text-align: center;\n \n@@ -93,8 +93,7 @@ input {\n }\n \n \n-@media (max-width: 500px) {\n+@media (max-width: 600px) {\n .generic-form-container {\n- width: 90%;\n }\n-}\n\\ No newline at end of file\n+}"} +{"repo": ".", "date": "2025-04-02", "line": "fix: Handle missing last message gracefully", "commit": "b365afc910522a484ad257af6df7b607712c71f2", "diff": "commit b365afc910522a484ad257af6df7b607712c71f2\nAuthor: retoor \nDate: Wed Apr 2 10:52:02 2025 +0200\n\n Update.\n\ndiff --git a/src/snek/model/channel.py b/src/snek/model/channel.py\nindex 183ddb0..0a90c39 100644\n--- a/src/snek/model/channel.py\n+++ b/src/snek/model/channel.py\n@@ -13,12 +13,15 @@ class ChannelModel(BaseModel):\n last_message_on = ModelField(name=\"last_message_on\", required=False, kind=str)\n \n async def get_last_message(self) -> ChannelMessageModel:\n- async for model in self.app.services.channel_message.query(\n- \"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1\",\n- {\"channel_uid\": self[\"uid\"]},\n- ):\n+ try:\n+ async for model in self.app.services.channel_message.query(\n+ \"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1\",\n+ {\"channel_uid\": self[\"uid\"]},\n+ ):\n \n- return await self.app.services.channel_message.get(uid=model[\"uid\"])\n+ return await self.app.services.channel_message.get(uid=model[\"uid\"])\n+ except:\n+ pass\n return None\n \n async def get_members(self):"} +{"repo": ".", "date": "2025-04-02", "line": "feat: Added unique session ID and updated styling for mobile responsiveness", "commit": "99b2beeab0d55242537b7ec810bfdd69feb47103", "diff": "commit 99b2beeab0d55242537b7ec810bfdd69feb47103\nAuthor: retoor \nDate: Wed Apr 2 14:55:18 2025 +0200\n\n Updates.\n\ndiff --git a/src/snek/app.py b/src/snek/app.py\nindex d043c0e..c6c2e2f 100644\n--- a/src/snek/app.py\n+++ b/src/snek/app.py\n@@ -2,7 +2,7 @@ import asyncio\n import logging\n import pathlib\n import time\n-\n+import uuid\n from snek.view.threads import ThreadsView\n \n logging.basicConfig(level=logging.DEBUG)\n@@ -189,6 +189,8 @@ class Application(BaseApplication):\n channels = []\n if not context:\n context = {}\n+ \n+ context['rid'] = str(uuid.uuid4())\n if request.session.get(\"uid\"):\n async for subscribed_channel in self.services.channel_member.find(\n user_uid=request.session.get(\"uid\"), deleted_at=None, is_banned=False\ndiff --git a/src/snek/static/base.css b/src/snek/static/base.css\nindex 7a4617d..049241d 100644\n--- a/src/snek/static/base.css\n+++ b/src/snek/static/base.css\n@@ -42,6 +42,7 @@ header {\n padding-top: 10px;\n padding-left: 20px;\n padding-right: 20px;\n+ padding-bottom: 10px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n@@ -79,6 +80,17 @@ a {\n margin-bottom: 3px;\n }\n \n+h1 {\n+ font-size: 2em;\n+}\n+\n+h2 {\n+ font-size: 1.4em;\n+}\n+\n+\n .chat-area {\n flex: 1;\n display: flex;\n@@ -354,7 +366,31 @@ a {\n }\n \n-@media only screen and (max-width: 600px) {\n+@media only screen and (max-width: 768px) {\n+ \n+ header{\n+ position:fixed;\n+ top: 0;\n+ left: 0;\n+ text-overflow: ellipsis;\n+\n+ *{\n+ font-size: 12px !important;\n+ }\n+ .logo {\n+ text-overflow: ellipsis;\n+ white-space: nowrap;\n+ overflow: hidden;\n+ h2 {\n+ font-size: 12px;\n+ }\n+ }\n+ \n+ }\n+ body {\n+ justify-content: flex-start;\n+ }\n header{\n position: sticky;\n display: block;\n@@ -364,5 +400,5 @@ a {\n }\n .chat-input {\n position:sticky;\n- }\n }\ndiff --git a/src/snek/static/generic-form.css b/src/snek/static/generic-form.css\nindex 7010193..025bfb3 100644\n--- a/src/snek/static/generic-form.css\n+++ b/src/snek/static/generic-form.css\n@@ -34,11 +34,11 @@ generic-form {\n text-align: center;\n \n }\n-\n .generic-form-container h1 {\n font-size: 2em;\n margin-bottom: 20px;\n+\n }\n input {\n \ndiff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js\nindex be4b327..f2edacb 100644\n--- a/src/snek/static/generic-form.js\n+++ b/src/snek/static/generic-form.js\n@@ -266,9 +266,9 @@ class GenericForm extends HTMLElement {\n }\n \n div {\n+ min-width:350px;\n border-radius: 10px;\n padding: 30px;\n- width: 400px;\n box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);\n text-align: center;\n }\ndiff --git a/src/snek/static/style.css b/src/snek/static/style.css\nindex 6e81345..af04db4 100644\n--- a/src/snek/static/style.css\n+++ b/src/snek/static/style.css\n@@ -56,3 +56,9 @@ div {\n text-align: left;\n \n }\n+\n+@media screen and (max-width: 500px) {\n+ body {\n+\n+ justify-content: flex-start;\n+ }\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 1533153..6c3f137 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -21,11 +21,12 @@\n \n \n+\n+\n+\n
\n-
Snek
\n- \n-
{% block header_text %}{% endblock %}
\n-
\n+\n+
\n+\n+
\n+
\n+

File Sharing

\n+
    \n+
  • SFTP storage with your Snek credentials
  • \n+
  • WebDAV support \u2013 same login
  • \n+
\n+
\n+\n+
\n+

Git Repositories

\n+
    \n+
  • Configure repos for any official Git client
  • \n+
  • Instant setup, push & pull
  • \n+
\n+
\n+\n+
\n+

AI Powerhouse

\n+
    \n+
  • Chat with free & commercial AIs
  • \n+
  • Generate AI-powered images
  • \n+
  • Build your own AI bots in <5 minutes (copy/paste example)
  • \n+
\n+
\n+\n+
\n+

Dev & Terminal

\n+
    \n+
  • Ubuntu web terminal in-browser
  • \n+
  • Full profile & permissions management
  • \n+
\n+
\n+\n+
\n+

Chat & Media

\n+
    \n+
  • Upload any file type in chat
  • \n+
  • Rich media support (audio, video, images\u2026)
  • \n+
  • Direct messaging with other users
  • \n+
\n+
\n+\n+
\n+

Privacy & Community

\n+
    \n+
  • No logging\u2014never even your IP
  • \n+
  • No email required to sign up
  • \n+
  • Multi-national, open community
  • \n+
  • Hacking encouraged!
  • \n+
\n+
\n+\n+
\n+

Customization & Deployment

\n+
    \n+
  • Full layout & theme customization
  • \n+
  • Install as a PWA on your phone
  • \n+
  • Optionally self-host: pip install snek, zero config
  • \n+
\n+
\n+
\n+\n+
\n+

Ready to join?

\n+

No email. No logs. Just sign up, pick a username, and dive in!

\n+ Sign Up Now\n+
\n+\n+
\n+

Self-Host in Seconds

\n+

Just run:

\n+snek serve\n+ \n+

No configuration required\u2014it's that simple.

\n+
\n+\n+
\n+\n+
\n+

© 2025 Snek \u2013 Join our global community of developers, testers & AI enthusiasts.

\n+
\n+\n \n "} +{"repo": ".", "date": "2025-05-17", "line": "feat: Refactor chat input component with improved auto-completion and live typing functionality", "commit": "48c3daf3983e3b6e04a0c5888febceb69db9d661", "diff": "commit 48c3daf3983e3b6e04a0c5888febceb69db9d661\nAuthor: retoor \nDate: Sat May 17 00:54:15 2025 +0200\n\n Update.\n\ndiff --git a/src/snek/static/chat-input.js b/src/snek/static/chat-input.js\nindex c1d767d..2d0914e 100644\n--- a/src/snek/static/chat-input.js\n+++ b/src/snek/static/chat-input.js\n@@ -1,69 +1,234 @@\n-\n-\n-\n-\n-class ChatInputElement extends HTMLElement {\n- _chatWindow = null \n- constructor() {\n- super();\n- this.attachShadow({ mode: 'open' });\n- this.component = document.createElement('div');\n- this.shadowRoot.appendChild(this.component);\n- }\n- set chatWindow(value){\n- this._chatWindow = value \n-\n- }\n- get chatWindow(){\n- return this._chatWindow \n- }\n- get channelUid() {\n- return this.chatWindow.channel.uid\n- }\n- connectedCallback() {\n- const link = document.createElement('link');\n- link.rel = 'stylesheet';\n- link.href = '/base.css';\n- this.component.appendChild(link);\n-\n- this.container = document.createElement('div');\n- this.container.classList.add('chat-input');\n- this.container.innerHTML = `\n- \n- \n- `;\n- this.textBox = this.container.querySelector('textarea');\n- this.uploadButton = this.container.querySelector('upload-button');\n- this.uploadButton.chatInput = this \n- this.textBox.addEventListener('input', (e) => {\n- this.dispatchEvent(new CustomEvent('input', { detail: e.target.value, bubbles: true }));\n- const message = e.target.value;\n- const button = this.container.querySelector('button');\n- button.disabled = !message;\n- });\n-\n- this.textBox.addEventListener('change', (e) => {\n- e.preventDefault();\n- this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));\n- console.error(e.target.value);\n- });\n-\n- this.textBox.addEventListener('keydown', (e) => {\n- if (e.key === 'Enter' && !e.shiftKey) {\n- e.preventDefault();\n- const message = e.target.value.trim();\n- if (!message) return;\n- this.dispatchEvent(new CustomEvent('submit', { detail: message, bubbles: true }));\n- e.target.value = '';\n- }\n- });\n-\n- this.component.appendChild(this.container);\n- }\n+\n+import { app } from '../app.js';\n+\n+class ChatInputComponent extends HTMLElement {\n+ autoCompletions = {\n+ 'example 1': () => {\n+\n+ },\n+ 'example 2': () => {\n+\n+ }\n+ }\n+\n+ constructor() {\n+ super();\n+ this.lastUpdateEvent = new Date();\n+ this.textarea = document.createElement(\"textarea\");\n+ this._value = \"\";\n+ this.value = this.getAttribute(\"value\") || \"\";\n+ this.previousValue = this.value;\n+ this.lastChange = new Date();\n+ this.changed = false;\n+ }\n+\n+ get value() {\n+ return this._value;\n+ }\n+\n+ set value(value) {\n+ this._value = value || \"\";\n+ this.textarea.value = this._value;\n+ }\n+\n+ resolveAutoComplete() {\n+ let count = 0;\n+ let value = null;\n+ Object.keys(this.autoCompletions).forEach((key) => {\n+ if (key.startsWith(this.value)) {\n+ count++;\n+ value = key;\n+ }\n+ });\n+ if (count == 1)\n+ return value;\n+ return null;\n+ }\n+\n+ isActive() {\n+ return document.activeElement === this.textarea;\n+ }\n+\n+ focus() {\n+ this.textarea.focus();\n+ }\n+\n+ connectedCallback() {\n+ this.liveType = this.getAttribute(\"live-type\") === \"true\";\n+ this.liveTypeInterval = parseInt(this.getAttribute(\"live-type-interval\")) || 3;\n+ this.channelUid = this.getAttribute(\"channel\");\n+ this.messageUid = null;\n+\n+ this.classList.add(\"chat-input\");\n+\n+ this.textarea.setAttribute(\"placeholder\", \"Type a message...\");\n+ this.textarea.setAttribute(\"rows\", \"2\");\n+\n+ this.appendChild(this.textarea);\n+\n+ this.uploadButton = document.createElement(\"upload-button\");\n+ this.uploadButton.setAttribute(\"channel\", this.channelUid);\n+ this.uploadButton.addEventListener(\"upload\", (e) => {\n+ this.dispatchEvent(new CustomEvent(\"upload\", e));\n+ });\n+ this.uploadButton.addEventListener(\"uploaded\", (e) => {\n+ this.dispatchEvent(new CustomEvent(\"uploaded\", e));\n+ });\n+\n+ this.appendChild(this.uploadButton);\n+\n+ this.textarea.addEventListener(\"keyup\", (e) => {\n+ if(e.key === 'Enter' && !e.shiftKey) {\n+ this.value = ''\n+ e.target.value = '';\n+ return \n+ }\n+ this.value = e.target.value;\n+ this.changed = true;\n+ this.update();\n+ });\n+\n+ this.textarea.addEventListener(\"keydown\", (e) => {\n+ this.value = e.target.value;\n+ if (e.key === \"Tab\") {\n+ e.preventDefault();\n+ let autoCompletion = this.resolveAutoComplete();\n+ if (autoCompletion) {\n+ e.target.value = autoCompletion;\n+ this.value = autoCompletion;\n+ return;\n+ }\n+ }\n+ if (e.key === 'Enter' && !e.shiftKey) {\n+ e.preventDefault();\n+\n+ const message = e.target.value;\n+ this.messageUid = null;\n+ this.value = '';\n+ this.previousValue = '';\n+\n+ if (!message) {\n+ return;\n+ }\n+\n+ let autoCompletion = this.autoCompletions[message];\n+ if (autoCompletion) {\n+ this.value = '';\n+ this.previousValue = '';\n+ e.target.value = '';\n+ autoCompletion();\n+ return;\n+ }\n+\n+ e.target.value = '';\n+ this.value = '';\n+ this.messageUid = null;\n+ this.sendMessage(this.channelUid, message).then((uid) => {\n+ this.messageUid = uid;\n+ });\n+ }\n+ });\n+\n+ this.changeInterval = setInterval(() => {\n+ if (!this.liveType) {\n+ return;\n+ }\n+ if (this.value !== this.previousValue) {\n+ if (this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= this.liveTypeInterval) {\n+ this.value = '';\n+ this.previousValue = '';\n+ }\n+ this.lastChange = new Date();\n+ }\n+ this.update();\n+ }, 300);\n+\n+ this.addEventListener(\"upload\", (e) => {\n+ this.focus();\n+ });\n+ this.addEventListener(\"uploaded\", function (e) {\n+ let message = \"\";\n+ e.detail.files.forEach((file) => {\n+ message += `[${file.name}](/channel/attachment/${file.relative_url})`;\n+ });\n+ app.rpc.sendMessage(this.channelUid, message);\n+ });\n+ }\n+\n+ trackSecondsBetweenEvents(event1Time, event2Time) {\n+ const millisecondsDifference = event2Time.getTime() - event1Time.getTime();\n+ return millisecondsDifference / 1000;\n+ }\n+\n+ newMessage() {\n+ if (!this.messageUid) {\n+ this.messageUid = '?';\n+ }\n+\n+ this.sendMessage(this.channelUid, this.value).then((uid) => {\n+ this.messageUid = uid;\n+ });\n+ }\n+\n+ updateMessage() {\n+ if (this.value[0] == \"/\") {\n+ return false;\n+ }\n+ if (!this.messageUid) {\n+ this.newMessage();\n+ return false;\n+ }\n+ if (this.messageUid === '?') {\n+ return false;\n+ }\n+ if (typeof app !== \"undefined\" && app.rpc && typeof app.rpc.updateMessageText === \"function\") {\n+ app.rpc.updateMessageText(this.messageUid, this.value);\n+ }\n+ }\n+\n+ updateStatus() {\n+ if (this.liveType) {\n+ return;\n+ }\n+ if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) {\n+ this.lastUpdateEvent = new Date();\n+ if (typeof app !== \"undefined\" && app.rpc && typeof app.rpc.set_typing === \"function\") {\n+ app.rpc.set_typing(this.channelUid);\n+ }\n+ }\n+ }\n+\n+ update() {\n+ const expired = this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= this.liveTypeInterval;\n+ const changed = (this.value !== this.previousValue);\n+\n+ if (changed || expired) {\n+ this.lastChange = new Date();\n+ this.updateStatus();\n+ }\n+\n+ this.previousValue = this.value;\n+\n+ if (this.liveType && expired) {\n+ this.value = \"\";\n+ this.previousValue = \"\";\n+ this.messageUid = null;\n+ return;\n+ }\n+\n+ if (changed) {\n+ if (this.liveType) {\n+ this.updateMessage();\n+ }\n+ }\n+ }\n+\n+ async sendMessage(channelUid, value) {\n+ if (!value.trim()) {\n+ return null;\n+ }\n+ return await app.rpc.sendMessage(channelUid, value);\n+ }\n }\n \n-customElements.define('chat-input', ChatInputElement);\n+customElements.define('chat-input', ChatInputComponent);\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 7baa67a..596b5d1 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -18,6 +18,7 @@\n \n \n \n+ \n \n \n \ndiff --git a/src/snek/templates/web.html b/src/snek/templates/web.html\nindex 02e60df..94d0ac5 100644\n--- a/src/snek/templates/web.html\n+++ b/src/snek/templates/web.html\n@@ -4,10 +4,6 @@\n \n {% block main %}\n \n-\n-\n-\n-\n
\n \n {% for message in messages %}\n@@ -16,10 +12,7 @@\n {% endautoescape %}\n {% endfor %}\n \n-
\n- \n- \n-
\n+ \n
\n {% include \"dialog_help.html\" %}\n {% include \"dialog_online.html\" %}\n@@ -27,11 +20,8 @@\n import { app } from \"/app.js\";\n import { Schedule } from \"/schedule.js\";\n const channelUid = \"{{ channel.uid.value }}\";\n-\n- function getInputField(){\n- return document.querySelector(\"textarea\")\n- }\n- getInputField().autoComplete = {\n+ const chatInputField = document.querySelector(\"chat-input\");\n+ chatInputField.autoCompletions = {\n \"/online\": () =>{\n showOnline();\n },\n@@ -39,117 +29,15 @@\n document.querySelector(\".chat-messages\").innerHTML = '';\n },\n \"/live\": () =>{\n- getInputField().liveType = !getInputField().liveType\n+ \n+ chatInputField.liveType = !chatInputField.liveType\n },\n \"/help\": () => {\n showHelp();\n }\n- }\n-\n-\n- function initInputField(textBox) {\n- if(textBox.liveType == undefined){\n- textBox.liveType = false\n- }\n- let typeTimeout = null;\n- textBox.addEventListener('keydown',async (e) => {\n- if(typeTimeout){\n- clearTimeout(typeTimeout)\n- typeTimeout = null\n- }\n- if(e.target.liveType){\n- typeTimeout = setTimeout(()=>{\n- e.target.lastMessageUid = null\n- e.target.value = ''\n- },3000)\n- }\n- if(e.key === \"ArrowUp\"){\n- const value = findDivAboveText(e.target.value).querySelector('.text')\n- e.target.value = value.textContent\n- console.info(\"HIERR\")\n- return\n- }\n- if (e.key === \"Tab\") {\n-\n- const message = e.target.value.trim();\n- if (!message) {\n- return\n- }\n- let autoCompleteHandler = null;\n- Object.keys(e.target.autoComplete).forEach((key)=>{\n- if(key.startsWith(message)){\n- if(autoCompleteHandler){\n- return \n- }\n- autoCompleteHandler = key\n- }\n- })\n- if(autoCompleteHandler){\n- e.preventDefault();\n- e.target.value = autoCompleteHandler;\n- return\n- }\n- }\n- if (e.key === 'Enter' && !e.shiftKey) {\n- e.preventDefault();\n- const message = e.target.value.trim();\n- if (!message) {\n- return\n- }\n- let autoCompleteHandler = e.target.autoComplete[message]\n- if(autoCompleteHandler){\n- const value = message;\n- e.target.value = '';\n- autoCompleteHandler(value)\n- return\n- }\n-\n- e.target.value = '';\n- if(textBox.liveType && textBox.lastMessageUid && textBox.lastMessageUid != '?'){\n- \n- \n- app.rpc.updateMessageText(textBox.lastMessageUid, message)\n- textBox.lastMessageUid = null\n- return \n- }\n-\n- const messageResponse = await app.rpc.sendMessage(channelUid, message);\n- \n-\t }else{\n-\t\tif(textBox.liveType){\n- if(e.target.value.endsWith(\"\\n\") || e.target.value.endsWith(\" \")){\n- return\n- } \n- if(e.target.value[0] == \"/\"){\n- return\n- }\n- if(!textBox.lastMessageUid){\n- textBox.lastMessageUid = '?'\n- app.rpc.sendMessage(channelUid, e.target.value).then((messageResponse)=>{\n- textBox.lastMessageUid = messageResponse\n- })\n- }\n- if(textBox.lastMessageUid == '?'){\n- return;\n- }\n- app.rpc.updateMessageText(textBox.lastMessageUid, e.target.value)\n- }else{\n- app.rpc.set_typing(channelUid)\n- }\n-\n- \n-\t }\n- });\n- document.querySelector(\"upload-button\").addEventListener(\"upload\",function(e){\n- getInputField().focus();\n- })\n- document.querySelector(\"upload-button\").addEventListener(\"uploaded\",function(e){\n- let message = \"\"\n- e.detail.files.forEach((file)=>{\n- message += `[${file.name}](/channel/attachment/${file.relative_url})`\n- })\n- app.rpc.sendMessage(channelUid,message)\n- })\n+ }\n+ \n+ const textBox = document.querySelector(\"chat-input\").textarea\n textBox.addEventListener(\"paste\", async (e) => {\n try {\n const clipboardItems = await navigator.clipboard.read();\n@@ -168,7 +56,7 @@\n }\n \n if (dt.items.length > 0) {\n- const uploadButton = document.querySelector(\"upload-button\");\n+ const uploadButton = chatInputField.uploadButton\n const input = uploadButton.shadowRoot.querySelector('.file-input')\n input.files = dt.files;\n \n@@ -187,7 +75,7 @@\n \n const dt = e.dataTransfer;\n if (dt.items.length > 0) {\n- const uploadButton = document.querySelector(\"upload-button\");\n+ const uploadButton = chatInputField.uploadButton\n const input = uploadButton.shadowRoot.querySelector('.file-input')\n input.files = dt.files;\n \n@@ -197,13 +85,16 @@\n chatInput.addEventListener(\"dragover\", async (e) => {\n e.preventDefault();\n e.dataTransfer.dropEffect = \"link\";\n+ \n+\n })\n \n- textBox.focus();\n- }\n+ chatInputField.textarea.focus();\n+\n+ \n \n function replyMessage(message) {\n- const field = getInputField()\n+ const field = chatInputField\n field.value = \"```markdown\\n> \" + (message || '') + \"\\n```\\n\";\n field.focus();\n }\n@@ -294,8 +185,8 @@\n lastMessage = messagesContainer.querySelector(\".message:last-child\");\n if (doScrollDown) {\n lastMessage?.scrollIntoView({ block: \"end\", inline: \"nearest\" });\n- const inputBox = document.querySelector(\".chat-input\");\n- inputBox.scrollIntoView({ block: \"end\", inline: \"nearest\" });\n+ \n+ chatInputField.scrollIntoView({ block: \"end\", inline: \"nearest\" });\n }\n }\n \n@@ -378,17 +269,17 @@\n messagesContainer.querySelector(\".message:last-child\").scrollIntoView({ block: \"end\", inline: \"nearest\" });\n setTimeout(() => {\n \n- getInputField().focus();\n+ chatInputField.focus();\n },500)\n \n }\n }\n if (event.shiftKey && event.key === 'G') {\n- if(document.activeElement != getInputField()){\n+ if(chatInputField.isActive()){\n \n updateLayout(true);\n setTimeout(() => {\n- getInputField().focus();\n+ chatInputField.focus();\n },500)\n }\n \n@@ -432,7 +323,6 @@\n document.body.removeChild(overlay);\n });\n });\n- initInputField(getInputField());\n updateLayout(true);\n \n {% endblock %}"} +{"repo": ".", "date": "2025-05-17", "line": "feat: Added star animation to sandbox page", "commit": "e79abf4a26454cddf766cd1ba138554817c820cb", "diff": "commit e79abf4a26454cddf766cd1ba138554817c820cb\nAuthor: retoor \nDate: Sat May 17 17:46:59 2025 +0200\n\n Update stars.\n\ndiff --git a/src/snek/static/sandbox.css b/src/snek/static/sandbox.css\nnew file mode 100644\nindex 0000000..1419fe4\n--- /dev/null\n+++ b/src/snek/static/sandbox.css\n@@ -0,0 +1,28 @@\n+ .star {\n+ position: absolute;\n+ width: 2px;\n+ height: 2px;\n+ border-radius: 50%;\n+ opacity: 0;\n+ animation: twinkle ease-in-out infinite;\n+ }\n+\n+ @keyframes twinkle {\n+ 0%, 100% { opacity: 0; }\n+ 50% { opacity: 1; }\n+ }\n+\n+ .content {\n+ position: relative;\n+ z-index: 1;\n+ font-family: sans-serif;\n+ text-align: center;\n+ top: 40%;\n+ transform: translateY(-40%);\n+ }\n+\ndiff --git a/src/snek/templates/app.html b/src/snek/templates/app.html\nindex 596b5d1..ceef196 100644\n--- a/src/snek/templates/app.html\n+++ b/src/snek/templates/app.html\n@@ -19,6 +19,7 @@\n \n \n \n+ \n \n \n \n@@ -78,5 +79,6 @@ let installPrompt = null\n \n ;\n \n+ {% include \"sandbox.html\" %}\n \n \ndiff --git a/src/snek/templates/sandbox.html b/src/snek/templates/sandbox.html\nnew file mode 100644\nindex 0000000..6fd9b3f\n--- /dev/null\n+++ b/src/snek/templates/sandbox.html\n@@ -0,0 +1,31 @@\n+\n+\t\t\t\t\t\t\t"} diff --git a/gitlog.py b/gitlog.py new file mode 100644 index 0000000..c0d6d95 --- /dev/null +++ b/gitlog.py @@ -0,0 +1,289 @@ +import http.server +import socketserver +import json +import os +import subprocess +from urllib.parse import parse_qs, urlparse +import mimetypes +import html + +# --- Theme selection (choose one: "light1", "light2", "dark1", "dark2") --- +THEME = "dark2" # Change this to "light2", "dark1", or "dark2" as desired + +THEMES = { + "light1": """ + body { font-family: Arial, sans-serif; background: #f6f8fa; margin: 0; padding: 0; color: #222; } + .container { max-width: 960px; margin: auto; padding: 2em; } + .date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 1px solid #ccc; } + .commit { margin: 1em 0; padding: 1em; background: #fff; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.07); } + .hash { color: #555; font-family: monospace; } + .diff { white-space: pre-wrap; background: #f1f1f1; padding: 1em; border-radius: 4px; overflow-x: auto; } + ul { list-style: none; padding-left: 0; } + li { margin: 0.3em 0; } + a { text-decoration: none; color: #0366d6; } + a:hover { text-decoration: underline; } + """, + "light2": """ + body { font-family: 'Segoe UI', sans-serif; background: #fdf6e3; margin: 0; padding: 0; color: #333; } + .container { max-width: 900px; margin: auto; padding: 2em; } + .date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 2px solid #e1c699; color: #b58900; } + .commit { margin: 1em 0; padding: 1em; background: #fffbe6; border-radius: 6px; box-shadow: 0 1px 3px rgba(200,180,100,0.08); } + .hash { color: #b58900; font-family: monospace; } + .diff { white-space: pre-wrap; background: #f5e9c9; padding: 1em; border-radius: 4px; overflow-x: auto; } + ul { list-style: none; padding-left: 0; } + li { margin: 0.3em 0; } + a { text-decoration: none; color: #b58900; } + a:hover { text-decoration: underline; color: #cb4b16; } + """, + "dark1": """ + body { font-family: Arial, sans-serif; background: #181a1b; margin: 0; padding: 0; color: #eaeaea; } + .container { max-width: 960px; margin: auto; padding: 2em; } + .date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 1px solid #333; color: #8ab4f8; } + .commit { margin: 1em 0; padding: 1em; background: #23272b; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.18); } + .hash { color: #8ab4f8; font-family: monospace; } + .diff { white-space: pre-wrap; background: #23272b; padding: 1em; border-radius: 4px; overflow-x: auto; } + ul { list-style: none; padding-left: 0; } + li { margin: 0.3em 0; } + a { text-decoration: none; color: #8ab4f8; } + a:hover { text-decoration: underline; color: #bb86fc; } + """, + "dark2": """ + body { font-family: 'Fira Sans', sans-serif; background: #121212; margin: 0; padding: 0; color: #d0d0d0; } + .container { max-width: 900px; margin: auto; padding: 2em; } + .date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 2px solid #444; color: #ffb86c; } + .commit { margin: 1em 0; padding: 1em; background: #22223b; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.22); } + .hash { color: #ffb86c; font-family: monospace; } + .diff { white-space: pre-wrap; background: #282a36; padding: 1em; border-radius: 4px; overflow-x: auto; } + ul { list-style: none; padding-left: 0; } + li { margin: 0.3em 0; } + a { text-decoration: none; color: #ffb86c; } + a:hover { text-decoration: underline; color: #8be9fd; } + """, +} + +def HTML_TEMPLATE(content, theme=THEME): + return f""" + + + + + Git Log Viewer + + + + + + +
+ {content} +
+ + +""" + +REPO_ROOT = os.path.abspath(".") +LOG_FILE = os.path.join(REPO_ROOT, "gitlog.jsonl") + +PORT = 8000 + +def format_diff_to_html(diff_text: str) -> str: + lines = diff_text.strip().splitlines() + html_lines = ['
'] + while not lines[3].startswith('diff'): + lines.pop(3) + lines.insert(3, "") + for line in lines: + escaped = html.escape(line) + if "//" in line: + continue + if "#" in line: + continue + if "/*" in line: + continue + if "*/" in line: + continue + if line.startswith('+++') or line.startswith('---'): + html_lines.append(f'
{escaped}
') + elif line.startswith('@@'): + html_lines.append(f'
{escaped}
') + elif line.startswith('+'): + html_lines.append(f'
{escaped}
') + elif line.startswith('-'): + html_lines.append(f'
{escaped}
') + elif line.startswith('\\'): + html_lines.append(f'
{escaped}
') + else: + html_lines.append(f'
{escaped}
') + html_lines.append('
') + return '\n'.join(html_lines) + +def parse_logs(): + logs = [] + if not os.path.exists(LOG_FILE): + return [] + lines = [] + with open(LOG_FILE, "r", encoding="utf-8") as f: + for line in f: + if line.strip(): + if line.strip() not in lines: + lines.append(line.strip()) + logs.append(json.loads(line.strip())) + return logs + +def group_by_date(logs): + grouped = {} + for entry in logs: + date = entry["date"] + grouped.setdefault(date, []).append(entry) + return dict(sorted(grouped.items(), reverse=True)) + +def get_git_diff(commit_hash): + try: + result = subprocess.run( + ["git", "-C", REPO_ROOT, "show", commit_hash, "--no-color"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True, + ) + return result.stdout + except subprocess.CalledProcessError as e: + return f"Error retrieving diff: {e.stderr}" + +def list_directory(path, base_url="/browse?path="): + try: + entries = os.listdir(path) + except OSError: + return "
Cannot access directory.
" + + entries.sort() + content = "
    " + # Parent directory link + parent = os.path.dirname(path) + if os.path.abspath(path) != REPO_ROOT: + parent_rel = os.path.relpath(parent, REPO_ROOT) + content += f"
  • .. (parent directory)
  • " + + for entry in entries: + full_path = os.path.join(path, entry) + rel_path = os.path.relpath(full_path, REPO_ROOT) + if os.path.isdir(full_path): + content += f"
  • 📁 {html.escape(entry)}/
  • " + else: + content += f"
  • 📄 {html.escape(entry)}
  • " + content += "
" + return content + +def read_file_content(path): + try: + with open(path, "r", encoding="utf-8") as f: + return f.read() + except Exception as e: + return f"Error reading file: {e}" + +def get_language_class(filename): + ext = os.path.splitext(filename)[1].lower() + return { + '.py': 'python', + '.js': 'javascript', + '.html': 'html', + '.css': 'css', + '.json': 'json', + '.sh': 'bash', + '.md': 'markdown', + '.c': 'c', + '.cpp': 'cpp', + '.h': 'cpp', + '.java': 'java', + '.rb': 'ruby', + '.go': 'go', + '.php': 'php', + '.rs': 'rust', + '.ts': 'typescript', + '.xml': 'xml', + '.yml': 'yaml', + '.yaml': 'yaml', + }.get(ext, '') + +class GitLogHandler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + parsed = urlparse(self.path) + if parsed.path == "/": + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + logs = parse_logs() + grouped = group_by_date(logs) + content = "

Browse Files

" + for date, commits in grouped.items(): + content += f"
{date}
" + for c in commits: + commit_link = f"/diff?hash={c['commit']}" + content += f""" +
+
{c['line'].splitlines()[0]}
+ +
+ """ + self.wfile.write(HTML_TEMPLATE(content).encode("utf-8")) + elif parsed.path == "/diff": + qs = parse_qs(parsed.query) + commit = qs.get("hash", [""])[0] + diff = format_diff_to_html(get_git_diff(html.escape(commit))) + diff_html = f"

Commit: {commit}

{diff}

← Back to commits

" + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write(HTML_TEMPLATE(diff_html).encode("utf-8")) + elif parsed.path == "/browse": + qs = parse_qs(parsed.query) + rel_path = qs.get("path", [""])[0] + abs_path = os.path.abspath(os.path.join(REPO_ROOT, rel_path)) + # Security: prevent escaping the repo root + if not abs_path.startswith(REPO_ROOT): + self.send_error(403, "Forbidden") + return + + if os.path.isdir(abs_path): + content = f"

Browsing: /{html.escape(rel_path)}

" + content += list_directory(abs_path) + content += "

← Back to commits

" + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write(HTML_TEMPLATE(content).encode("utf-8")) + elif os.path.isfile(abs_path): + file_content = read_file_content(abs_path) + lang_class = get_language_class(abs_path) + content = f"

File: /{html.escape(rel_path)}

" + content += ( + f"
"
+                    f"{html.escape(file_content)}
" + ) + content += "

← Back to directory

".format( + f"/browse?path={html.escape(os.path.dirname(rel_path))}" + ) + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write(HTML_TEMPLATE(content).encode("utf-8")) + else: + self.send_error(404, "Not found") + else: + self.send_error(404) + +if __name__ == "__main__": + + while True: + try: + with socketserver.TCPServer(("", PORT), GitLogHandler) as httpd: + print(f"Serving at http://localhost:{PORT}") + httpd.serve_forever() + break + except Exception as ex: + print(ex) + PORT += 1 + +