Merge branch 'main' into feat/push-notifications

# Conflicts:
#	src/snek/app.py
This commit is contained in:
BordedDev 2025-06-01 12:43:37 +02:00
commit fcc2d7b748
No known key found for this signature in database
GPG Key ID: C5F495EAE56673BF
7 changed files with 80 additions and 21 deletions

View File

@ -38,6 +38,7 @@ dependencies = [
"humanize", "humanize",
"Pillow", "Pillow",
"pillow-heif", "pillow-heif",
"IP2Location",
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]

Binary file not shown.

View File

@ -2,14 +2,13 @@ import asyncio
import logging import logging
import pathlib import pathlib
import ssl import ssl
import time
import uuid import uuid
from datetime import datetime from datetime import datetime
from snek import snode from snek import snode
from snek.view.threads import ThreadsView from snek.view.threads import ThreadsView
import json
logging.basicConfig(level=logging.DEBUG)
logging.basicConfig(level=logging.DEBUG)
from ipaddress import ip_address
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from aiohttp import web from aiohttp import web
@ -21,7 +20,7 @@ from aiohttp_session import (
from aiohttp_session.cookie_storage import EncryptedCookieStorage from aiohttp_session.cookie_storage import EncryptedCookieStorage
from app.app import Application as BaseApplication from app.app import Application as BaseApplication
from jinja2 import FileSystemLoader from jinja2 import FileSystemLoader
import IP2Location
from snek.sssh import start_ssh_server from snek.sssh import start_ssh_server
from snek.docs.app import Application as DocsApplication from snek.docs.app import Application as DocsApplication
@ -60,9 +59,15 @@ from snek.view.user import UserView
from snek.view.web import WebView from snek.view.web import WebView
from snek.view.channel import ChannelAttachmentView from snek.view.channel import ChannelAttachmentView
from snek.view.channel import ChannelView from snek.view.channel import ChannelView
from snek.view.settings.containers import ContainersIndexView, ContainersCreateView, ContainersUpdateView, ContainersDeleteView from snek.view.settings.containers import (
ContainersIndexView,
ContainersCreateView,
ContainersUpdateView,
ContainersDeleteView,
)
from snek.webdav import WebdavApplication from snek.webdav import WebdavApplication
from snek.sgit import GitApplication from snek.sgit import GitApplication
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34" SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
@ -73,6 +78,33 @@ async def session_middleware(request, handler):
return response return response
@web.middleware
async def ip2location_middleware(request, handler):
response = await handler(request)
return response
ip = request.headers.get("X-Forwarded-For", request.remote)
ipaddress = ip_address(ip)
if ipaddress.is_private:
return response
if not request.app.session.get("uid"):
return response
user = await request.app.services.user.get(uid=request.app.session.get("uid"))
if not user:
return response
location = request.app.ip2location.get(ip)
original_city = user["city"]
if user["city"] != location.city:
user["country_long"] = location.country
user["country_short"] = locaion.country_short
user["city"] = location.city
user["region"] = location.region
user["latitude"] = location.latitude
user["longitude"] = location.longitude
user["ip"] = ip
await request.app.services.user.update(user)
return response
@web.middleware @web.middleware
async def trailing_slash_middleware(request, handler): async def trailing_slash_middleware(request, handler):
if request.path and not request.path.endswith("/"): if request.path and not request.path.endswith("/"):
@ -82,16 +114,19 @@ async def trailing_slash_middleware(request, handler):
class Application(BaseApplication): class Application(BaseApplication):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
middlewares = [ middlewares = [
cors_middleware, cors_middleware,
web.normalize_path_middleware(merge_slashes=True), web.normalize_path_middleware(merge_slashes=True),
ip2location_middleware,
] ]
self.template_path = pathlib.Path(__file__).parent.joinpath("templates") self.template_path = pathlib.Path(__file__).parent.joinpath("templates")
self.static_path = pathlib.Path(__file__).parent.joinpath("static") self.static_path = pathlib.Path(__file__).parent.joinpath("static")
super().__init__( super().__init__(
middlewares=middlewares, template_path=self.template_path, client_max_size=1024*1024*1024*5 *args, **kwargs middlewares=middlewares,
template_path=self.template_path,
client_max_size=1024 * 1024 * 1024 * 5 * args,
**kwargs,
) )
session_setup(self, EncryptedCookieStorage(SESSION_KEY)) session_setup(self, EncryptedCookieStorage(SESSION_KEY))
self.tasks = asyncio.Queue() self.tasks = asyncio.Queue()
@ -105,7 +140,6 @@ class Application(BaseApplication):
self.ssh_host = "0.0.0.0" self.ssh_host = "0.0.0.0"
self.ssh_port = 2242 self.ssh_port = 2242
self.setup_router() self.setup_router()
self.ssh_server = None self.ssh_server = None
self.sync_service = None self.sync_service = None
@ -115,7 +149,10 @@ class Application(BaseApplication):
self.mappers = get_mappers(app=self) self.mappers = get_mappers(app=self)
self.broadcast_service = None self.broadcast_service = None
self.user_availability_service_task = None self.user_availability_service_task = None
base_path = pathlib.Path(__file__).parent
self.ip2location = IP2Location.IP2Location(
base_path.joinpath("IP2LOCATION-LITE-DB11.BIN")
)
self.on_startup.append(self.prepare_asyncio) self.on_startup.append(self.prepare_asyncio)
self.on_startup.append(self.start_user_availability_service) self.on_startup.append(self.start_user_availability_service)
self.on_startup.append(self.start_ssh_server) self.on_startup.append(self.start_ssh_server)
@ -129,7 +166,7 @@ class Application(BaseApplication):
def uptime(self): def uptime(self):
return self._format_uptime(self.uptime_seconds) return self._format_uptime(self.uptime_seconds)
def _format_uptime(self,seconds): def _format_uptime(self, seconds):
seconds = int(seconds) seconds = int(seconds)
days, seconds = divmod(seconds, 86400) days, seconds = divmod(seconds, 86400)
hours, seconds = divmod(seconds, 3600) hours, seconds = divmod(seconds, 3600)
@ -147,14 +184,16 @@ class Application(BaseApplication):
return ", ".join(parts) return ", ".join(parts)
async def start_user_availability_service(self, app): async def start_user_availability_service(self, app):
app.user_availability_service_task = asyncio.create_task(app.services.socket.user_availability_service()) app.user_availability_service_task = asyncio.create_task(
app.services.socket.user_availability_service()
)
async def snode_sync(self, app): async def snode_sync(self, app):
self.sync_service = asyncio.create_task(snode.sync_service(app)) self.sync_service = asyncio.create_task(snode.sync_service(app))
async def start_ssh_server(self, app): async def start_ssh_server(self, app):
app.ssh_server = await start_ssh_server(app,app.ssh_host,app.ssh_port) app.ssh_server = await start_ssh_server(app, app.ssh_host, app.ssh_port)
if app.ssh_server: if app.ssh_server:
asyncio.create_task(app.ssh_server.wait_closed()) asyncio.create_task(app.ssh_server.wait_closed())
@ -253,12 +292,11 @@ class Application(BaseApplication):
self.webdav = WebdavApplication(self) self.webdav = WebdavApplication(self)
self.git = GitApplication(self) self.git = GitApplication(self)
self.add_subapp("/webdav", self.webdav) self.add_subapp("/webdav", self.webdav)
self.add_subapp("/git",self.git) self.add_subapp("/git", self.git)
#self.router.add_get("/{file_path:.*}", self.static_handler) # self.router.add_get("/{file_path:.*}", self.static_handler)
async def handle_test(self, request): async def handle_test(self, request):
return await self.render_template( return await self.render_template(
"test.html", request, context={"name": "retoor"} "test.html", request, context={"name": "retoor"}
) )
@ -285,7 +323,6 @@ class Application(BaseApplication):
async for subscribed_channel in self.services.channel_member.find( async for subscribed_channel in self.services.channel_member.find(
user_uid=request.session.get("uid"), deleted_at=None, is_banned=False user_uid=request.session.get("uid"), deleted_at=None, is_banned=False
): ):
parent_object = await subscribed_channel.get_channel() parent_object = await subscribed_channel.get_channel()
item = {} item = {}
@ -335,9 +372,8 @@ class Application(BaseApplication):
return rendered return rendered
async def static_handler(self, request): async def static_handler(self, request):
file_name = request.match_info.get('filename', '') file_name = request.match_info.get("filename", "")
paths = [] paths = []
@ -371,7 +407,6 @@ class Application(BaseApplication):
if user_template_path: if user_template_path:
template_paths.append(user_template_path) template_paths.append(user_template_path)
template_paths.append(self.template_path) template_paths.append(self.template_path)
return FileSystemLoader(template_paths) return FileSystemLoader(template_paths)

View File

@ -31,6 +31,14 @@ class UserModel(BaseModel):
is_admin = ModelField(name="is_admin", required=False, kind=bool) is_admin = ModelField(name="is_admin", required=False, kind=bool)
country_short = ModelField(name="country_short", required=False, kind=str)
country_long = ModelField(name="country_long", required=False, kind=str)
city = ModelField(name="city", required=False, kind=str)
latitude = ModelField(name="latitude", required=False, kind=float)
longitude = ModelField(name="longitude", required=False, kind=float)
region = ModelField(name="region", required=False, kind=str)
ip = ModelField(name="ip", required=False, kind=str)
async def get_property(self, name): async def get_property(self, name):
prop = await self.app.services.user_property.find_one( prop = await self.app.services.user_property.find_one(
user_uid=self["uid"], name=name user_uid=self["uid"], name=name

View File

@ -23,15 +23,23 @@ class MessageList extends HTMLElement {
const messagesContainer = this const messagesContainer = this
messagesContainer.addEventListener('click', (e) => { messagesContainer.addEventListener('click', (e) => {
if (e.target.tagName !== 'IMG' || e.target.classList.contains('avatar-img')) return; if (e.target.tagName !== 'IMG' || e.target.classList.contains('avatar-img')) return;
const img = e.target; const img = e.target;
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;' overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;'
const urlObj = new URL(img.currentSrc || img.src)
urlObj.searchParams.delete("width");
urlObj.searchParams.delete("height");
const fullImg = document.createElement('img'); const fullImg = document.createElement('img');
const urlObj = new URL(img.src); urlObj.search = '';
fullImg.src = urlObj.toString(); fullImg.src = urlObj.toString();
fullImg.alt = img.alt; fullImg.alt = img.alt;
fullImg.style.maxWidth = '90%'; fullImg.style.maxWidth = '90%';
fullImg.style.maxHeight = '90%'; fullImg.style.maxHeight = '90%';
overlay.appendChild(fullImg); overlay.appendChild(fullImg);
document.body.appendChild(overlay); document.body.appendChild(overlay);
overlay.addEventListener('click', () => document.body.removeChild(overlay)); overlay.addEventListener('click', () => document.body.removeChild(overlay));

View File

@ -74,6 +74,10 @@ class BaseMapper:
for record in await self.run_in_executor(self.db.query,sql, *args): for record in await self.run_in_executor(self.db.query,sql, *args):
yield dict(record) yield dict(record)
async def update(self, model):
model.updated_at.update()
return await self.run_in_executor(self.table.update, model.record, ["uid"])
async def delete(self, **kwargs) -> int: async def delete(self, **kwargs) -> int:
if not kwargs or not isinstance(kwargs, dict): if not kwargs or not isinstance(kwargs, dict):
raise Exception("Can't execute delete with no filter.") raise Exception("Can't execute delete with no filter.")

View File

@ -26,6 +26,9 @@ class BaseService:
kwargs["uid"] = uid kwargs["uid"] = uid
return await self.count(**kwargs) > 0 return await self.count(**kwargs) > 0
async def update(self, model):
return await self.mapper.update(model)
async def count(self, **kwargs): async def count(self, **kwargs):
return await self.mapper.count(**kwargs) return await self.mapper.count(**kwargs)