diff --git a/.editorconfig b/.editorconfig index deb2bac..6b67635 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,5 +4,5 @@ root = true indent_size = 4 end_of_line = lf charset = utf-8 -trim_trailing_whitespace=true +trim_trailing_whitespace = true insert_final_newline = true diff --git a/README.md b/README.md index 1cec162..a2fc77c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ ## `inbox`: Сервер для внутренних переписок ### ENV - - REDIS_URL - - AUTH_URL - - API_BASE + +- REDIS_URL +- AUTH_URL +- API_BASE ### Как это работает __Redis__: - - Для каждого пользователя создаётся запись в хранилищах `chats_by_author/` и `chats/` и канал redis `chat:`, в котором публикуюутся обновления всех переписок. + +- Для каждого пользователя создаётся запись в хранилищах `chats_by_author/` и `chats/` и канал + redis `chat:`, в котором публикуюутся обновления всех переписок. diff --git a/main.py b/main.py index fc5343a..82c9fa2 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,11 @@ import os from importlib import import_module from os.path import exists + from ariadne import load_schema_from_path, make_executable_schema from ariadne.asgi import GraphQL from starlette.applications import Starlette + from services.redis import redis from services.schema import resolvers from settings import DEV_SERVER_PID_FILE_NAME, SENTRY_DSN, MODE diff --git a/pyproject.toml b/pyproject.toml index 480e5c6..30fd568 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = ["Tony Rewin "] [tool.poetry.dependencies] python = "^3.12" sentry-sdk = "^1.32.0" -redis = {extras = ["hiredis"], version = "^5.0.1"} +redis = { extras = ["hiredis"], version = "^5.0.1" } ariadne = "^0.20.1" starlette = "^0.31.1" uvicorn = "^0.23.0" @@ -20,6 +20,7 @@ itsdangerous = "^2.1.2" [tool.poetry.dev-dependencies] pytest = "^7.4.2" +black = { version = "^23.9.1", python = ">=3.12" } [tool.black] line-length = 120 diff --git a/resolvers/__init__.py b/resolvers/__init__.py index 51222e6..6c70e55 100644 --- a/resolvers/__init__.py +++ b/resolvers/__init__.py @@ -1,14 +1,13 @@ from resolvers.chats import create_chat, delete_chat, update_chat +from resolvers.load import load_chats, load_messages_by, load_recipients from resolvers.messages import ( create_message, delete_message, update_message, mark_as_read, ) -from resolvers.load import load_chats, load_messages_by, load_recipients from resolvers.search import search_recipients - __all__ = [ # inbox "load_chats", diff --git a/resolvers/chats.py b/resolvers/chats.py index c7ed614..ed94b67 100644 --- a/resolvers/chats.py +++ b/resolvers/chats.py @@ -1,10 +1,11 @@ import json import uuid from datetime import datetime, timezone -from validators.inbox import Chat + from services.auth import login_required from services.redis import redis from services.schema import mutation +from validators.inbox import Chat @mutation.field("updateChat") @@ -107,4 +108,3 @@ async def delete_chat(_, info, chat_id: str): await redis.execute("SREM", f"chats_by_author/{author_id}", chat_id) else: return {"error": "chat not exist"} - diff --git a/resolvers/load.py b/resolvers/load.py index 530dfdd..a15a1dd 100644 --- a/resolvers/load.py +++ b/resolvers/load.py @@ -1,13 +1,15 @@ +import asyncio import json from typing import Any, Dict, List, Optional, Union + +from services.auth import login_required from services.core import get_author, get_network from services.redis import redis -from services.auth import login_required from services.schema import query from validators.inbox import Message, Chat, ChatMember from .chats import create_chat from .unread import get_unread_counter -import asyncio + # NOTE: not an API handler async def load_messages( @@ -21,10 +23,10 @@ async def load_messages( message_ids = [] + ids if limit: mids = ( - await redis.lrange( - f"chats/{chat_id}/message_ids", offset, offset + limit - ) - ) or [] + await redis.lrange( + f"chats/{chat_id}/message_ids", offset, offset + limit + ) + ) or [] mids = [mid for mid in mids] message_ids += mids if message_ids: @@ -54,7 +56,7 @@ async def load_chats( author_id = info.context["author_id"] cids = (await redis.execute("SMEMBERS", f"chats_by_author/{author_id}")) or [] members_online = (await redis.execute("SMEMBERS", "authors-online")) or [] - cids = list(cids)[offset : (offset + limit)] + cids = list(cids)[offset: (offset + limit)] chats = [] lock = asyncio.Lock() if len(cids) == 0: @@ -87,8 +89,8 @@ async def load_messages_by(_, info, by, limit: int = 10, offset: int = 0): """load :limit messages of :chat_id with :offset""" author_id = info.context["author_id"] user_chats = ( - await redis.execute("SMEMBERS", "chats_by_author/" + str(author_id)) - ) or [] + await redis.execute("SMEMBERS", "chats_by_author/" + str(author_id)) + ) or [] user_chats = [c for c in user_chats] if user_chats: messages = [] diff --git a/resolvers/messages.py b/resolvers/messages.py index d0dfbae..7a6e22c 100644 --- a/resolvers/messages.py +++ b/resolvers/messages.py @@ -1,11 +1,12 @@ import json from datetime import datetime, timezone from typing import List -from validators.inbox import Message + from services.auth import login_required from services.presence import notify_message from services.redis import redis from services.schema import mutation +from validators.inbox import Message @mutation.field("createMessage") @@ -26,16 +27,15 @@ async def create_message(_, info, chat: str, body: str, reply_to=None): else: chat_dict = json.loads(chat_data) print(chat_dict) - message_id = ( - await redis.execute("GET", f"chats/{chat_dict['id']}/next_message_id") - ) or 0 - message_id = int(message_id) + message_id = await redis.execute("GET", f"chats/{chat_dict['id']}/next_message_id") + message_id = int(message_id) if message_id else 0 new_message: Message = { "chat": chat_dict["id"], "id": message_id, "author": author_id, "body": body, "createdAt": int(datetime.now(tz=timezone.utc).timestamp()), + "updatedAt": None } if reply_to: new_message["replyTo"] = reply_to @@ -110,10 +110,10 @@ async def delete_message(_, info, chat_id: str, message_id: int): return {"error": "chat not exist"} chat = json.loads(chat) - message = await redis.execute("GET", f"chats/{chat_id}/messages/{str(message_id)}") - if not message: + message_data = await redis.execute("GET", f"chats/{chat_id}/messages/{str(message_id)}") + if not message_data: return {"error": "message not exist"} - message: Message = json.loads(message) + message: Message = json.loads(message_data) if message["author"] != author_id: return {"error": "access denied"} diff --git a/resolvers/search.py b/resolvers/search.py index 2ffc848..2431d3d 100644 --- a/resolvers/search.py +++ b/resolvers/search.py @@ -1,10 +1,11 @@ import json -from typing import Dict, Union, List, Any from datetime import datetime, timezone, timedelta +from typing import Dict, Union, List, Any + +from resolvers.load import load_messages from services.auth import login_required from services.core import get_network from services.redis import redis -from resolvers.load import load_messages from services.schema import query @@ -14,11 +15,11 @@ async def search_recipients(_, info, text: str, limit: int = 50, offset: int = 0 result = [] # TODO: maybe redis scan? - + author_id = info.context["author_id"] talk_before = await redis.execute("GET", f"/chats_by_author/{author_id}") if talk_before: - talk_before = list(json.loads(talk_before))[offset : (offset + limit)] + talk_before = list(json.loads(talk_before))[offset: (offset + limit)] for chat_id in talk_before: members = await redis.execute("GET", f"/chats/{chat_id}/members") if members: @@ -68,8 +69,8 @@ async def search_in_chats( mmm = list( filter( lambda msg: int(datetime.now(tz=timezone.utc)) - - int(msg["createdAt"]) - < timedelta(days=days_ago), + - int(msg["createdAt"]) + < timedelta(days=days_ago), mmm, ) ) diff --git a/resolvers/unread.py b/resolvers/unread.py index 3bd061d..150ec02 100644 --- a/resolvers/unread.py +++ b/resolvers/unread.py @@ -1,7 +1,7 @@ -from services.redis import redis import json from services.auth import login_required +from services.redis import redis async def get_unread_counter(chat_id: str, author_id: int) -> int: diff --git a/server.py b/server.py index 045c188..96425e5 100644 --- a/server.py +++ b/server.py @@ -1,4 +1,5 @@ import sys + import uvicorn from uvicorn.main import logger diff --git a/services/auth.py b/services/auth.py index 6f244be..073ea92 100644 --- a/services/auth.py +++ b/services/auth.py @@ -1,8 +1,9 @@ import json from functools import wraps -from httpx import AsyncClient, HTTPError -from settings import AUTH_URL +from httpx import AsyncClient, HTTPError + +from settings import AUTH_URL INTERNAL_AUTH_SERVER = "v2.discours" in AUTH_URL or "testapi.discours" in AUTH_URL @@ -19,12 +20,12 @@ async def check_auth(req): gql = { "query": query_type - + " " - + operation - + " { " - + query_name - + " { user { id } } " - + " }", + + " " + + operation + + " { " + + query_name + + " { user { id } } " + + " }", "operationName": operation, "variables": None, } diff --git a/services/presence.py b/services/presence.py index e35c177..14b89e8 100644 --- a/services/presence.py +++ b/services/presence.py @@ -1,4 +1,5 @@ import json + from services.redis import redis from validators.inbox import Message diff --git a/services/redis.py b/services/redis.py index 5282471..d878609 100644 --- a/services/redis.py +++ b/services/redis.py @@ -1,4 +1,5 @@ import redis.asyncio as aredis + from settings import REDIS_URL diff --git a/validators/inbox.py b/validators/inbox.py index be7185e..cc275a8 100644 --- a/validators/inbox.py +++ b/validators/inbox.py @@ -1,6 +1,7 @@ -from typing import Dict, Optional, List +from typing import TypedDict, Optional, List -class Message(Dict): + +class Message(TypedDict): id: int chat: str author: int @@ -10,20 +11,22 @@ class Message(Dict): createdAt: int updatedAt: Optional[int] -class Chat(Dict): + +class Chat(TypedDict): id: str members: List[int] admins: List[int] title: str - updatedAt: int + updatedAt: Optional[int] createdAt: int createdBy: int description: Optional[str] -class ChatMember(Dict): + +class ChatMember(TypedDict): id: int slug: str name: str userpic: Optional[str] lastSeen: int - online: Optional[bool] \ No newline at end of file + online: Optional[bool]