This commit is contained in:
commit
e942d82412
|
@ -1,3 +1,7 @@
|
||||||
|
[0.2.22]
|
||||||
|
- precommit installed
|
||||||
|
- granian asgi added
|
||||||
|
|
||||||
[0.2.19]
|
[0.2.19]
|
||||||
- versioning sync with core, inbox, presence
|
- versioning sync with core, inbox, presence
|
||||||
- fix: auth connection user_id trimming
|
- fix: auth connection user_id trimming
|
||||||
|
|
13
main.py
13
main.py
|
@ -16,22 +16,23 @@ from services.rediscache import redis
|
||||||
from settings import DEV_SERVER_PID_FILE_NAME, MODE, SENTRY_DSN
|
from settings import DEV_SERVER_PID_FILE_NAME, MODE, SENTRY_DSN
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logger = logging.getLogger("\t[main]\t")
|
||||||
logger = logging.getLogger('\t[main]\t')
|
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
async def start_up():
|
async def start_up():
|
||||||
|
logger.info("[main] starting...")
|
||||||
await redis.connect()
|
await redis.connect()
|
||||||
|
|
||||||
task = asyncio.create_task(notifications_worker())
|
task = asyncio.create_task(notifications_worker())
|
||||||
logger.info(task)
|
logger.info(task)
|
||||||
|
|
||||||
if MODE == 'dev':
|
if MODE == "dev":
|
||||||
if exists(DEV_SERVER_PID_FILE_NAME):
|
if exists(DEV_SERVER_PID_FILE_NAME):
|
||||||
with open(DEV_SERVER_PID_FILE_NAME, 'w', encoding='utf-8') as f:
|
with open(DEV_SERVER_PID_FILE_NAME, "w", encoding="utf-8") as f:
|
||||||
f.write(str(os.getpid()))
|
f.write(str(os.getpid()))
|
||||||
else:
|
else:
|
||||||
|
logger.info("[main] production mode")
|
||||||
try:
|
try:
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@ async def start_up():
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('sentry init error', e)
|
logger.error("sentry init error", e)
|
||||||
|
|
||||||
|
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
|
@ -54,4 +55,4 @@ async def shutdown():
|
||||||
|
|
||||||
|
|
||||||
app = Starlette(debug=True, on_startup=[start_up], on_shutdown=[shutdown])
|
app = Starlette(debug=True, on_startup=[start_up], on_shutdown=[shutdown])
|
||||||
app.mount('/', GraphQL(schema, debug=True))
|
app.mount("/", GraphQL(schema, debug=True))
|
||||||
|
|
|
@ -34,6 +34,7 @@ async def handle_notification(n: ServiceMessage, channel: str):
|
||||||
|
|
||||||
|
|
||||||
async def listen_task(pattern):
|
async def listen_task(pattern):
|
||||||
|
logger.info(f' listening {pattern} ...')
|
||||||
async for message_data, channel in redis.listen(pattern):
|
async for message_data, channel in redis.listen(pattern):
|
||||||
try:
|
try:
|
||||||
if message_data:
|
if message_data:
|
||||||
|
|
|
@ -8,7 +8,12 @@ from sqlalchemy import and_, select
|
||||||
from sqlalchemy.orm import aliased
|
from sqlalchemy.orm import aliased
|
||||||
from sqlalchemy.sql import not_
|
from sqlalchemy.sql import not_
|
||||||
|
|
||||||
from orm.notification import Notification, NotificationAction, NotificationEntity, NotificationSeen
|
from orm.notification import (
|
||||||
|
Notification,
|
||||||
|
NotificationAction,
|
||||||
|
NotificationEntity,
|
||||||
|
NotificationSeen,
|
||||||
|
)
|
||||||
from resolvers.model import (
|
from resolvers.model import (
|
||||||
NotificationAuthor,
|
NotificationAuthor,
|
||||||
NotificationGroup,
|
NotificationGroup,
|
||||||
|
@ -19,15 +24,22 @@ from resolvers.model import (
|
||||||
from services.db import local_session
|
from services.db import local_session
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('[resolvers.schema]')
|
logger = logging.getLogger("[resolvers.schema]")
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
def query_notifications(author_id: int, after: int = 0) -> Tuple[int, int, List[Tuple[Notification, bool]]]:
|
def query_notifications(
|
||||||
|
author_id: int, after: int = 0
|
||||||
|
) -> Tuple[int, int, List[Tuple[Notification, bool]]]:
|
||||||
notification_seen_alias = aliased(NotificationSeen)
|
notification_seen_alias = aliased(NotificationSeen)
|
||||||
query = select(Notification, notification_seen_alias.viewer.label('seen')).outerjoin(
|
query = select(
|
||||||
|
Notification, notification_seen_alias.viewer.label("seen")
|
||||||
|
).outerjoin(
|
||||||
NotificationSeen,
|
NotificationSeen,
|
||||||
and_(NotificationSeen.viewer == author_id, NotificationSeen.notification == Notification.id),
|
and_(
|
||||||
|
NotificationSeen.viewer == author_id,
|
||||||
|
NotificationSeen.notification == Notification.id,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if after:
|
if after:
|
||||||
query = query.filter(Notification.created_at > after)
|
query = query.filter(Notification.created_at > after)
|
||||||
|
@ -36,7 +48,12 @@ def query_notifications(author_id: int, after: int = 0) -> Tuple[int, int, List[
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
total = (
|
total = (
|
||||||
session.query(Notification)
|
session.query(Notification)
|
||||||
.filter(and_(Notification.action == NotificationAction.CREATE.value, Notification.created_at > after))
|
.filter(
|
||||||
|
and_(
|
||||||
|
Notification.action == NotificationAction.CREATE.value,
|
||||||
|
Notification.created_at > after,
|
||||||
|
)
|
||||||
|
)
|
||||||
.count()
|
.count()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -63,7 +80,9 @@ def query_notifications(author_id: int, after: int = 0) -> Tuple[int, int, List[
|
||||||
def process_shout_notification(
|
def process_shout_notification(
|
||||||
notification: Notification, seen: bool
|
notification: Notification, seen: bool
|
||||||
) -> Union[Tuple[str, NotificationGroup], None] | None:
|
) -> Union[Tuple[str, NotificationGroup], None] | None:
|
||||||
if not isinstance(notification.payload, str) or not isinstance(notification.entity, str):
|
if not isinstance(notification.payload, str) or not isinstance(
|
||||||
|
notification.entity, str
|
||||||
|
):
|
||||||
return
|
return
|
||||||
payload = json.loads(notification.payload)
|
payload = json.loads(notification.payload)
|
||||||
shout: NotificationShout = payload
|
shout: NotificationShout = payload
|
||||||
|
@ -75,7 +94,7 @@ def process_shout_notification(
|
||||||
authors=shout.authors,
|
authors=shout.authors,
|
||||||
updated_at=shout.created_at,
|
updated_at=shout.created_at,
|
||||||
reactions=[],
|
reactions=[],
|
||||||
action='create',
|
action="create",
|
||||||
seen=seen,
|
seen=seen,
|
||||||
)
|
)
|
||||||
return thread_id, group
|
return thread_id, group
|
||||||
|
@ -94,8 +113,8 @@ def process_reaction_notification(
|
||||||
reaction: NotificationReaction = payload
|
reaction: NotificationReaction = payload
|
||||||
shout: NotificationShout = reaction.shout
|
shout: NotificationShout = reaction.shout
|
||||||
thread_id = str(reaction.shout)
|
thread_id = str(reaction.shout)
|
||||||
if reaction.kind == 'COMMENT' and reaction.reply_to:
|
if reaction.kind == "COMMENT" and reaction.reply_to:
|
||||||
thread_id += f'::{reaction.reply_to}'
|
thread_id += f"::{reaction.reply_to}"
|
||||||
group = NotificationGroup(
|
group = NotificationGroup(
|
||||||
id=thread_id,
|
id=thread_id,
|
||||||
action=str(notification.action),
|
action=str(notification.action),
|
||||||
|
@ -116,15 +135,15 @@ def process_follower_notification(
|
||||||
return
|
return
|
||||||
payload = json.loads(notification.payload)
|
payload = json.loads(notification.payload)
|
||||||
follower: NotificationAuthor = payload
|
follower: NotificationAuthor = payload
|
||||||
thread_id = 'followers'
|
thread_id = "followers"
|
||||||
group = NotificationGroup(
|
group = NotificationGroup(
|
||||||
id=thread_id,
|
id=thread_id,
|
||||||
authors=[follower],
|
authors=[follower],
|
||||||
updated_at=int(time.time()),
|
updated_at=int(time.time()),
|
||||||
shout=None,
|
shout=None,
|
||||||
reactions=[],
|
reactions=[],
|
||||||
entity='follower',
|
entity="follower",
|
||||||
action='follow',
|
action="follow",
|
||||||
seen=seen,
|
seen=seen,
|
||||||
)
|
)
|
||||||
return thread_id, group
|
return thread_id, group
|
||||||
|
@ -133,6 +152,31 @@ def process_follower_notification(
|
||||||
async def get_notifications_grouped(
|
async def get_notifications_grouped(
|
||||||
author_id: int, after: int = 0, limit: int = 10
|
author_id: int, after: int = 0, limit: int = 10
|
||||||
) -> Tuple[Dict[str, NotificationGroup], int, int]:
|
) -> Tuple[Dict[str, NotificationGroup], int, int]:
|
||||||
|
"""
|
||||||
|
Retrieves notifications for a given author.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
author_id (int): The ID of the author for whom notifications are retrieved.
|
||||||
|
after (int, optional): If provided, selects only notifications created after this timestamp will be considered.
|
||||||
|
limit (int, optional): The maximum number of groupa to retrieve.
|
||||||
|
offset (int, optional): Offset for pagination
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, NotificationGroup], int, int: A dictionary where keys are thread IDs and values are NotificationGroup objects, unread and total amounts.
|
||||||
|
|
||||||
|
This function queries the database to retrieve notifications for the specified author, considering optional filters.
|
||||||
|
The result is a dictionary where each key is a thread ID, and the corresponding value is a NotificationGroup
|
||||||
|
containing information about the notifications within that thread.
|
||||||
|
|
||||||
|
NotificationGroup structure:
|
||||||
|
{
|
||||||
|
entity: str, # Type of entity (e.g., 'reaction', 'shout', 'follower').
|
||||||
|
updated_at: int, # Timestamp of the latest update in the thread.
|
||||||
|
shout: Optional[NotificationShout]
|
||||||
|
reactions: List[int], # List of reaction ids within the thread.
|
||||||
|
authors: List[NotificationAuthor], # List of authors involved in the thread.
|
||||||
|
}
|
||||||
|
"""
|
||||||
total, unread, notifications = query_notifications(author_id, after)
|
total, unread, notifications = query_notifications(author_id, after)
|
||||||
groups_by_thread: Dict[str, NotificationGroup] = {}
|
groups_by_thread: Dict[str, NotificationGroup] = {}
|
||||||
groups_amount = 0
|
groups_amount = 0
|
||||||
|
@ -141,7 +185,7 @@ async def get_notifications_grouped(
|
||||||
if groups_amount >= limit:
|
if groups_amount >= limit:
|
||||||
break
|
break
|
||||||
|
|
||||||
if str(notification.entity) == 'shout' and str(notification.action) == 'create':
|
if str(notification.entity) == "shout" and str(notification.action) == "create":
|
||||||
result = process_shout_notification(notification, seen)
|
result = process_shout_notification(notification, seen)
|
||||||
if result:
|
if result:
|
||||||
thread_id, group = result
|
thread_id, group = result
|
||||||
|
@ -168,7 +212,7 @@ async def get_notifications_grouped(
|
||||||
groups_by_thread[thread_id] = group
|
groups_by_thread[thread_id] = group
|
||||||
groups_amount += 1
|
groups_amount += 1
|
||||||
|
|
||||||
elif str(notification.entity) == 'follower':
|
elif str(notification.entity) == "follower":
|
||||||
result = process_follower_notification(notification, seen)
|
result = process_follower_notification(notification, seen)
|
||||||
if result:
|
if result:
|
||||||
thread_id, group = result
|
thread_id, group = result
|
||||||
|
@ -181,10 +225,18 @@ async def get_notifications_grouped(
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
class Query:
|
class Query:
|
||||||
@strawberry.field
|
@strawberry.field
|
||||||
async def load_notifications(self, info, after: int, limit: int = 50, offset: int = 0) -> NotificationsResult:
|
async def load_notifications(
|
||||||
author_id = info.context.get('author_id')
|
self, info, after: int, limit: int = 50, offset: int = 0
|
||||||
|
) -> NotificationsResult:
|
||||||
|
author_id = info.context.get("author_id")
|
||||||
if author_id:
|
if author_id:
|
||||||
groups, unread, total = await get_notifications_grouped(author_id, after, limit)
|
groups, unread, total = await get_notifications_grouped(
|
||||||
notifications = sorted(groups.values(), key=lambda group: group.updated_at, reverse=True)
|
author_id, after, limit
|
||||||
return NotificationsResult(notifications=notifications, total=total, unread=unread, error=None)
|
)
|
||||||
|
notifications = sorted(
|
||||||
|
groups.values(), key=lambda group: group.updated_at, reverse=True
|
||||||
|
)
|
||||||
|
return NotificationsResult(
|
||||||
|
notifications=notifications, total=total, unread=unread, error=None
|
||||||
|
)
|
||||||
return NotificationsResult(notifications=[], total=0, unread=0, error=None)
|
return NotificationsResult(notifications=[], total=0, unread=0, error=None)
|
||||||
|
|
69
server.py
69
server.py
|
@ -1,71 +1,14 @@
|
||||||
import logging
|
from granian.constants import Interfaces
|
||||||
import sys
|
from granian.server import Granian
|
||||||
|
|
||||||
from settings import PORT
|
from settings import PORT
|
||||||
|
|
||||||
|
|
||||||
log_settings = {
|
if __name__ == "__main__":
|
||||||
'version': 1,
|
print("[server] started")
|
||||||
'disable_existing_loggers': True,
|
|
||||||
'formatters': {
|
|
||||||
'default': {
|
|
||||||
'()': 'uvicorn.logging.DefaultFormatter',
|
|
||||||
'fmt': '%(levelprefix)s %(message)s',
|
|
||||||
'use_colors': None,
|
|
||||||
},
|
|
||||||
'access': {
|
|
||||||
'()': 'uvicorn.logging.AccessFormatter',
|
|
||||||
'fmt': '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'handlers': {
|
|
||||||
'default': {
|
|
||||||
'formatter': 'default',
|
|
||||||
'class': 'logging.StreamHandler',
|
|
||||||
'stream': 'ext://sys.stderr',
|
|
||||||
},
|
|
||||||
'access': {
|
|
||||||
'formatter': 'access',
|
|
||||||
'class': 'logging.StreamHandler',
|
|
||||||
'stream': 'ext://sys.stdout',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'loggers': {
|
|
||||||
'uvicorn': {'handlers': ['default'], 'level': 'INFO'},
|
|
||||||
'uvicorn.error': {'level': 'INFO', 'handlers': ['default'], 'propagate': True},
|
|
||||||
'uvicorn.access': {'handlers': ['access'], 'level': 'INFO', 'propagate': False},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
local_headers = [
|
|
||||||
('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, HEAD'),
|
|
||||||
('Access-Control-Allow-Origin', 'https://localhost:3000'),
|
|
||||||
(
|
|
||||||
'Access-Control-Allow-Headers',
|
|
||||||
'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization',
|
|
||||||
),
|
|
||||||
('Access-Control-Expose-Headers', 'Content-Length,Content-Range'),
|
|
||||||
('Access-Control-Allow-Credentials', 'true'),
|
|
||||||
]
|
|
||||||
|
|
||||||
logger = logging.getLogger('[server] ')
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
def exception_handler(_et, exc, _tb):
|
|
||||||
logger.error(..., exc_info=(type(exc), exc, exc.__traceback__))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.excepthook = exception_handler
|
|
||||||
from granian.constants import Interfaces
|
|
||||||
from granian.server import Granian
|
|
||||||
|
|
||||||
print('[server] started')
|
|
||||||
|
|
||||||
granian_instance = Granian(
|
granian_instance = Granian(
|
||||||
'main:app',
|
"main:app",
|
||||||
address='0.0.0.0', # noqa S104
|
address="0.0.0.0", # noqa S104
|
||||||
port=PORT,
|
port=PORT,
|
||||||
workers=2,
|
workers=2,
|
||||||
threads=2,
|
threads=2,
|
||||||
|
|
|
@ -8,53 +8,62 @@ from services.db import local_session
|
||||||
from settings import AUTH_URL
|
from settings import AUTH_URL
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('\t[services.auth]\t')
|
logger = logging.getLogger("\t[services.auth]\t")
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
async def check_auth(req) -> str | None:
|
async def check_auth(req) -> str | None:
|
||||||
token = req.headers.get('Authorization')
|
token = req.headers.get("Authorization")
|
||||||
user_id = ''
|
user_id = ""
|
||||||
if token:
|
if token:
|
||||||
query_name = 'validate_jwt_token'
|
query_name = "validate_jwt_token"
|
||||||
operation = 'ValidateToken'
|
operation = "ValidateToken"
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
variables = {
|
variables = {
|
||||||
'params': {
|
"params": {
|
||||||
'token_type': 'access_token',
|
"token_type": "access_token",
|
||||||
'token': token,
|
"token": token,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gql = {
|
gql = {
|
||||||
'query': f'query {operation}($params: ValidateJWTTokenInput!) {{ {query_name}(params: $params) {{ is_valid claims }} }}',
|
"query": f"query {operation}($params: ValidateJWTTokenInput!) {{ {query_name}(params: $params) {{ is_valid claims }} }}",
|
||||||
'variables': variables,
|
"variables": variables,
|
||||||
'operationName': operation,
|
"operationName": operation,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
# Asynchronous HTTP request to the authentication server
|
# Asynchronous HTTP request to the authentication server
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
async with session.post(AUTH_URL, json=gql, headers=headers) as response:
|
async with session.post(
|
||||||
print(f'[services.auth] HTTP Response {response.status} {await response.text()}')
|
AUTH_URL, json=gql, headers=headers
|
||||||
|
) as response:
|
||||||
|
logger.debug(
|
||||||
|
f"HTTP Response {response.status} {await response.text()}"
|
||||||
|
)
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
data = await response.json()
|
data = await response.json()
|
||||||
errors = data.get('errors')
|
errors = data.get("errors")
|
||||||
if errors:
|
if errors:
|
||||||
print(f'[services.auth] errors: {errors}')
|
logger.error(f"errors: {errors}")
|
||||||
else:
|
else:
|
||||||
user_id = data.get('data', {}).get(query_name, {}).get('claims', {}).get('sub')
|
user_id = (
|
||||||
|
data.get("data", {})
|
||||||
|
.get(query_name, {})
|
||||||
|
.get("claims", {})
|
||||||
|
.get("sub")
|
||||||
|
)
|
||||||
if user_id:
|
if user_id:
|
||||||
print(f'[services.auth] got user_id: {user_id}')
|
logger.info(f"got user_id: {user_id}")
|
||||||
return user_id
|
return user_id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
# Handling and logging exceptions during authentication check
|
# Handling and logging exceptions during authentication check
|
||||||
print(f'[services.auth] Error {e}')
|
logger.error(f"Error {e}")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -62,14 +71,14 @@ async def check_auth(req) -> str | None:
|
||||||
class LoginRequiredMiddleware(Extension):
|
class LoginRequiredMiddleware(Extension):
|
||||||
async def on_request_start(self):
|
async def on_request_start(self):
|
||||||
context = self.execution_context.context
|
context = self.execution_context.context
|
||||||
req = context.get('request')
|
req = context.get("request")
|
||||||
user_id = await check_auth(req)
|
user_id = await check_auth(req)
|
||||||
if user_id:
|
if user_id:
|
||||||
context['user_id'] = user_id.strip()
|
context["user_id"] = user_id.strip()
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
author = session.query(Author).filter(Author.user == user_id).first()
|
author = session.query(Author).filter(Author.user == user_id).first()
|
||||||
if author:
|
if author:
|
||||||
context['author_id'] = author.id
|
context["author_id"] = author.id
|
||||||
context['user_id'] = user_id or None
|
context["user_id"] = user_id or None
|
||||||
|
|
||||||
self.execution_context.context = context
|
self.execution_context.context = context
|
||||||
|
|
Loading…
Reference in New Issue
Block a user