diff --git a/pyproject.toml b/pyproject.toml index 00a4edb..a7c762f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "humanize", "Pillow", "pillow-heif", + "IP2Location", ] [tool.setuptools.packages.find] diff --git a/src/snek/IP2LOCATION-LITE-DB11.BIN b/src/snek/IP2LOCATION-LITE-DB11.BIN new file mode 100755 index 0000000..8f04af8 Binary files /dev/null and b/src/snek/IP2LOCATION-LITE-DB11.BIN differ diff --git a/src/snek/app.py b/src/snek/app.py index ee82144..4158680 100644 --- a/src/snek/app.py +++ b/src/snek/app.py @@ -2,14 +2,13 @@ import asyncio import logging import pathlib import ssl -import time import uuid from datetime import datetime from snek import snode 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 aiohttp import web @@ -21,7 +20,7 @@ from aiohttp_session import ( from aiohttp_session.cookie_storage import EncryptedCookieStorage from app.app import Application as BaseApplication from jinja2 import FileSystemLoader - +import IP2Location from snek.sssh import start_ssh_server 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.channel import ChannelAttachmentView 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.sgit import GitApplication + SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34" @@ -73,6 +78,33 @@ async def session_middleware(request, handler): 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 async def trailing_slash_middleware(request, handler): if request.path and not request.path.endswith("/"): @@ -82,16 +114,19 @@ async def trailing_slash_middleware(request, handler): class Application(BaseApplication): - def __init__(self, *args, **kwargs): middlewares = [ cors_middleware, web.normalize_path_middleware(merge_slashes=True), + ip2location_middleware, ] self.template_path = pathlib.Path(__file__).parent.joinpath("templates") self.static_path = pathlib.Path(__file__).parent.joinpath("static") 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)) self.tasks = asyncio.Queue() @@ -105,7 +140,6 @@ class Application(BaseApplication): self.ssh_host = "0.0.0.0" self.ssh_port = 2242 - self.setup_router() self.ssh_server = None self.sync_service = None @@ -115,7 +149,10 @@ class Application(BaseApplication): self.mappers = get_mappers(app=self) self.broadcast_service = 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.start_user_availability_service) self.on_startup.append(self.start_ssh_server) @@ -129,7 +166,7 @@ class Application(BaseApplication): def uptime(self): return self._format_uptime(self.uptime_seconds) - def _format_uptime(self,seconds): + def _format_uptime(self, seconds): seconds = int(seconds) days, seconds = divmod(seconds, 86400) hours, seconds = divmod(seconds, 3600) @@ -147,14 +184,16 @@ class Application(BaseApplication): return ", ".join(parts) - 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): self.sync_service = asyncio.create_task(snode.sync_service(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: asyncio.create_task(app.ssh_server.wait_closed()) @@ -253,12 +292,11 @@ class Application(BaseApplication): self.webdav = WebdavApplication(self) self.git = GitApplication(self) 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): - return await self.render_template( "test.html", request, context={"name": "retoor"} ) @@ -285,7 +323,6 @@ class Application(BaseApplication): async for subscribed_channel in self.services.channel_member.find( user_uid=request.session.get("uid"), deleted_at=None, is_banned=False ): - parent_object = await subscribed_channel.get_channel() item = {} @@ -335,9 +372,8 @@ class Application(BaseApplication): return rendered - async def static_handler(self, request): - file_name = request.match_info.get('filename', '') + file_name = request.match_info.get("filename", "") paths = [] @@ -371,7 +407,6 @@ class Application(BaseApplication): if user_template_path: template_paths.append(user_template_path) - template_paths.append(self.template_path) return FileSystemLoader(template_paths) diff --git a/src/snek/model/user.py b/src/snek/model/user.py index 9869456..afa4b53 100644 --- a/src/snek/model/user.py +++ b/src/snek/model/user.py @@ -30,6 +30,14 @@ class UserModel(BaseModel): last_ping = ModelField(name="last_ping", required=False, kind=str) 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): prop = await self.app.services.user_property.find_one( diff --git a/src/snek/static/message-list.js b/src/snek/static/message-list.js index 70c3573..c6430f2 100644 --- a/src/snek/static/message-list.js +++ b/src/snek/static/message-list.js @@ -23,15 +23,23 @@ class MessageList extends HTMLElement { const messagesContainer = this messagesContainer.addEventListener('click', (e) => { if (e.target.tagName !== 'IMG' || e.target.classList.contains('avatar-img')) return; + const img = e.target; + 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;' + + const urlObj = new URL(img.currentSrc || img.src) + urlObj.searchParams.delete("width"); + urlObj.searchParams.delete("height"); + const fullImg = document.createElement('img'); - const urlObj = new URL(img.src); urlObj.search = ''; + fullImg.src = urlObj.toString(); fullImg.alt = img.alt; fullImg.style.maxWidth = '90%'; fullImg.style.maxHeight = '90%'; + overlay.appendChild(fullImg); document.body.appendChild(overlay); overlay.addEventListener('click', () => document.body.removeChild(overlay)); diff --git a/src/snek/system/mapper.py b/src/snek/system/mapper.py index 8543cec..a0e37d2 100644 --- a/src/snek/system/mapper.py +++ b/src/snek/system/mapper.py @@ -74,6 +74,10 @@ class BaseMapper: for record in await self.run_in_executor(self.db.query,sql, *args): 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: if not kwargs or not isinstance(kwargs, dict): raise Exception("Can't execute delete with no filter.") diff --git a/src/snek/system/service.py b/src/snek/system/service.py index c6d2afc..bf0c9d6 100644 --- a/src/snek/system/service.py +++ b/src/snek/system/service.py @@ -26,6 +26,9 @@ class BaseService: kwargs["uid"] = uid return await self.count(**kwargs) > 0 + async def update(self, model): + return await self.mapper.update(model) + async def count(self, **kwargs): return await self.mapper.count(**kwargs)