diff --git a/auth/authenticate.py b/auth/authenticate.py index 93468de7..e11e821e 100644 --- a/auth/authenticate.py +++ b/auth/authenticate.py @@ -9,7 +9,7 @@ from starlette.requests import HTTPConnection from auth.credentials import AuthCredentials, AuthUser from base.orm import local_session -from orm.user import User, Role, UserRole +from orm.user import User, Role from settings import SESSION_TOKEN_HEADER from auth.tokenstorage import SessionToken @@ -39,12 +39,14 @@ class JWTAuthenticate(AuthenticationBackend): user = None with local_session() as session: try: - q = select( - User - ).filter( - User.id == payload.user_id - ).select_from(User) - user = session.execute(q).unique().one() + user = ( + session.query(User).options( + joinedload(User.roles).options(joinedload(Role.permissions)), + joinedload(User.ratings) + ).filter( + User.id == payload.user_id + ).one() + ) except exc.NoResultFound: user = None @@ -59,7 +61,7 @@ class JWTAuthenticate(AuthenticationBackend): scopes=scopes, logged_in=True ), - user, + AuthUser(user_id=user.id), ) else: InvalidToken("please try again") diff --git a/auth/email.py b/auth/email.py index 8a1f8dfd..7ca5d9bf 100644 --- a/auth/email.py +++ b/auth/email.py @@ -10,7 +10,7 @@ lang_subject = { } -async def send_auth_email(user, token, template="email_confirmation", lang="ru"): +async def send_auth_email(user, token, lang="ru", template="email_confirmation"): try: to = "%s <%s>" % (user.name, user.email) if lang not in ['ru', 'en']: diff --git a/main.py b/main.py index 7b8fcee9..5a5b7b92 100644 --- a/main.py +++ b/main.py @@ -41,6 +41,7 @@ async def start_up(): async def dev_start_up(): if exists(DEV_SERVER_STATUS_FILE_NAME): + await redis.connect() return else: with open(DEV_SERVER_STATUS_FILE_NAME, 'w', encoding='utf-8') as f: @@ -71,6 +72,7 @@ app.mount("/", GraphQL(schema, debug=True)) dev_app = app = Starlette( debug=True, on_startup=[dev_start_up], + on_shutdown=[shutdown], middleware=middleware, routes=routes, ) diff --git a/migration/tables/comments.py b/migration/tables/comments.py index 4fde9569..88c41216 100644 --- a/migration/tables/comments.py +++ b/migration/tables/comments.py @@ -9,7 +9,6 @@ from orm.shout import ShoutReactionsFollower from orm.topic import TopicFollower from orm.user import User from orm.shout import Shout -# from services.stat.reacted import ReactedStorage ts = datetime.now(tz=timezone.utc) @@ -84,7 +83,6 @@ def migrate_ratings(session, entry, reaction_dict): ) session.add(following2) session.add(rr) - # await ReactedStorage.react(rr) except Exception as e: print("[migration] comment rating error: %r" % re_reaction_dict) diff --git a/migration/tables/content_items.py b/migration/tables/content_items.py index c85b2cdc..030154ff 100644 --- a/migration/tables/content_items.py +++ b/migration/tables/content_items.py @@ -9,7 +9,6 @@ from orm.reaction import Reaction, ReactionKind from orm.shout import Shout, ShoutTopic, ShoutReactionsFollower from orm.user import User from orm.topic import TopicFollower, Topic -# from services.stat.reacted import ReactedStorage from services.stat.viewed import ViewedStorage import re @@ -365,7 +364,6 @@ async def content_ratings_to_reactions(entry, slug): else: rea = Reaction.create(**reaction_dict) session.add(rea) - # await ReactedStorage.react(rea) # shout_dict['ratings'].append(reaction_dict) session.commit() diff --git a/migration/tables/users.py b/migration/tables/users.py index b30e82c8..53b574d9 100644 --- a/migration/tables/users.py +++ b/migration/tables/users.py @@ -35,11 +35,12 @@ def migrate(entry): slug = entry["profile"].get("path").lower() slug = re.sub('[^0-9a-zA-Z]+', '-', slug).strip() user_dict["slug"] = slug - bio = BeautifulSoup(entry.get("profile").get("bio") or "", features="lxml").text - if bio.startswith('<'): - print('[migration] bio! ' + bio) - bio = BeautifulSoup(bio, features="lxml").text - bio = bio.replace('\(', '(').replace('\)', ')') + bio = (entry.get("profile", {"bio": ""}).get("bio") or "").replace('\(', '(').replace('\)', ')') + bio_html = BeautifulSoup(bio, features="lxml").text + if bio == bio_html: + user_dict["bio"] = bio + else: + user_dict["about"] = bio # userpic try: diff --git a/orm/user.py b/orm/user.py index f196f4f9..40d04799 100644 --- a/orm/user.py +++ b/orm/user.py @@ -56,7 +56,8 @@ class User(Base): email = Column(String, unique=True, nullable=False, comment="Email") username = Column(String, nullable=False, comment="Login") password = Column(String, nullable=True, comment="Password") - bio = Column(String, nullable=True, comment="Bio") + bio = Column(String, nullable=True, comment="Bio") # status description + about = Column(String, nullable=True, comment="About") # long and formatted userpic = Column(String, nullable=True, comment="Userpic") name = Column(String, nullable=True, comment="Display name") slug = Column(String, unique=True, comment="User's slug") @@ -100,7 +101,7 @@ class User(Base): session.commit() User.default_user = default - async def get_permission(self): + def get_permission(self): scope = {} for role in self.roles: for p in role.permissions: diff --git a/resolvers/auth.py b/resolvers/auth.py index 3cbd5977..7734df04 100644 --- a/resolvers/auth.py +++ b/resolvers/auth.py @@ -8,6 +8,7 @@ from starlette.responses import RedirectResponse from transliterate import translit from auth.authenticate import login_required +from auth.credentials import AuthCredentials from auth.email import send_auth_email from auth.identity import Identity, Password from auth.jwtcodec import JWTCodec @@ -24,20 +25,19 @@ from settings import SESSION_TOKEN_HEADER, FRONTEND_URL @mutation.field("getSession") @login_required async def get_current_user(_, info): - user = info.context["request"].user - token = info.context["request"].headers.get("Authorization") - if user and token: + auth: AuthCredentials = info.context["request"].auth + token = info.context["request"].headers.get(SESSION_TOKEN_HEADER) + + with local_session() as session: + user = session.query(User).where(User.id == auth.user_id).one() user.lastSeen = datetime.now(tz=timezone.utc) - with local_session() as session: - session.add(user) - session.commit() - return { - "token": token, - "user": user, - "news": await user_subscriptions(user.slug), - } - else: - raise Unauthorized("No session token present in request, try to login") + session.commit() + + return { + "token": token, + "user": user, + "news": await user_subscriptions(user.id), + } @mutation.field("confirmEmail") @@ -58,7 +58,7 @@ async def confirm_email(_, info, token): return { "token": session_token, "user": user, - "news": await user_subscriptions(user.slug) + "news": await user_subscriptions(user.id) } except InvalidToken as e: raise InvalidToken(e.message) @@ -174,7 +174,7 @@ async def login(_, info, email: str, password: str = "", lang: str = "ru"): return { "token": session_token, "user": user, - "news": await user_subscriptions(user.slug), + "news": await user_subscriptions(user.id), } except InvalidPassword: print(f"[auth] {email}: invalid password") diff --git a/resolvers/create/collab.py b/resolvers/create/collab.py index feff66ba..df509452 100644 --- a/resolvers/create/collab.py +++ b/resolvers/create/collab.py @@ -1,4 +1,5 @@ from auth.authenticate import login_required +from auth.credentials import AuthCredentials from base.orm import local_session from base.resolvers import query, mutation from base.exceptions import ObjectNotExist, BaseHttpException @@ -10,26 +11,28 @@ from orm.user import User @query.field("getCollabs") @login_required async def get_collabs(_, info): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth + with local_session() as session: - collabs = session.query(Collab).filter(user.slug in Collab.authors) + collabs = session.query(Collab).filter(auth.user_id in Collab.authors) return collabs @mutation.field("inviteCoauthor") @login_required async def invite_coauthor(_, info, author: str, shout: int): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth + with local_session() as session: s = session.query(Shout).where(Shout.id == shout).one() if not s: raise ObjectNotExist("invalid shout id") else: c = session.query(Collab).where(Collab.shout == shout).one() - if user.slug not in c.authors: + if auth.user_id not in c.authors: raise BaseHttpException("you are not in authors list") else: - invited_user = session.query(User).where(User.slug == author).one() + invited_user = session.query(User).where(User.id == author).one() c.invites.append(invited_user) session.add(c) session.commit() @@ -41,16 +44,17 @@ async def invite_coauthor(_, info, author: str, shout: int): @mutation.field("removeCoauthor") @login_required async def remove_coauthor(_, info, author: str, shout: int): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth + with local_session() as session: s = session.query(Shout).where(Shout.id == shout).one() if not s: raise ObjectNotExist("invalid shout id") - if user.slug != s.createdBy.slug: - raise BaseHttpException("only onwer can remove coauthors") + if auth.user_id != s.createdBy: + raise BaseHttpException("only owner can remove coauthors") else: c = session.query(Collab).where(Collab.shout == shout).one() - ca = session.query(CollabAuthor).where(c.shout == shout, c.author == author).one() + ca = session.query(CollabAuthor).join(User).where(c.shout == shout, User.slug == author).one() session.remve(ca) c.invites = filter(lambda x: x.slug == author, c.invites) c.authors = filter(lambda x: x.slug == author, c.authors) @@ -64,14 +68,15 @@ async def remove_coauthor(_, info, author: str, shout: int): @mutation.field("acceptCoauthor") @login_required async def accept_coauthor(_, info, shout: int): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth + with local_session() as session: s = session.query(Shout).where(Shout.id == shout).one() if not s: raise ObjectNotExist("invalid shout id") else: c = session.query(Collab).where(Collab.shout == shout).one() - accepted = filter(lambda x: x.slug == user.slug, c.invites).pop() + accepted = filter(lambda x: x.id == auth.user_id, c.invites).pop() if accepted: c.authors.append(accepted) s.authors.append(accepted) diff --git a/resolvers/create/editor.py b/resolvers/create/editor.py index 8205369b..82ccaf57 100644 --- a/resolvers/create/editor.py +++ b/resolvers/create/editor.py @@ -3,6 +3,7 @@ from datetime import datetime, timezone from sqlalchemy import and_ from auth.authenticate import login_required +from auth.credentials import AuthCredentials from base.orm import local_session from base.resolvers import mutation from orm.rbac import Resource @@ -19,7 +20,7 @@ from orm.collab import Collab @mutation.field("createShout") @login_required async def create_shout(_, info, inp): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth topic_slugs = inp.get("topic_slugs", []) if topic_slugs: @@ -37,24 +38,24 @@ async def create_shout(_, info, inp): "mainTopic": inp.get("topics", []).pop(), "visibility": "authors" }) - authors.remove(user.slug) + authors.remove(auth.user_id) if authors: chat = create_chat(None, info, new_shout.title, members=authors) # create a cooperative chatroom - MessagesStorage.register_chat(chat) + await MessagesStorage.register_chat(chat) # now we should create a collab new_collab = Collab.create({ "shout": new_shout.id, - "authors": [user.slug, ], + "authors": [auth.user_id, ], "invites": authors }) session.add(new_collab) # NOTE: shout made by one first author - sa = ShoutAuthor.create(shout=new_shout.id, user=user.id) + sa = ShoutAuthor.create(shout=new_shout.id, user=auth.user_id) session.add(sa) - reactions_follow(user, new_shout.slug, True) + reactions_follow(auth.user_id, new_shout.slug, True) if "mainTopic" in inp: topic_slugs.append(inp["mainTopic"]) @@ -65,11 +66,11 @@ async def create_shout(_, info, inp): st = ShoutTopic.create(shout=new_shout.id, topic=topic.id) session.add(st) tf = session.query(TopicFollower).where( - and_(TopicFollower.follower == user.id, TopicFollower.topic == topic.id) + and_(TopicFollower.follower == auth.user_id, TopicFollower.topic == topic.id) ) if not tf: - tf = TopicFollower.create(follower=user.id, topic=topic.id, auto=True) + tf = TopicFollower.create(follower=auth.user_id, topic=topic.id, auto=True) session.add(tf) new_shout.topic_slugs = topic_slugs @@ -77,7 +78,8 @@ async def create_shout(_, info, inp): session.commit() - GitTask(inp, user.username, user.email, "new shout %s" % new_shout.slug) + # TODO + # GitTask(inp, user.username, user.email, "new shout %s" % new_shout.slug) return {"shout": new_shout} @@ -85,18 +87,17 @@ async def create_shout(_, info, inp): @mutation.field("updateShout") @login_required async def update_shout(_, info, inp): - auth = info.context["request"].auth - user_id = auth.user_id + auth: AuthCredentials = info.context["request"].auth slug = inp["slug"] with local_session() as session: - user = session.query(User).filter(User.id == user_id).first() + user = session.query(User).filter(User.id == auth.user_id).first() shout = session.query(Shout).filter(Shout.slug == slug).first() if not shout: return {"error": "shout not found"} authors = [author.id for author in shout.authors] - if user_id not in authors: + if auth.user_id not in authors: scopes = auth.scopes print(scopes) if Resource.shout not in scopes: @@ -115,7 +116,7 @@ async def update_shout(_, info, inp): ShoutTopic.create(shout=slug, topic=topic) session.commit() - GitTask(inp, user.username, user.email, "update shout %s" % (slug)) + GitTask(inp, user.username, user.email, "update shout %s" % slug) return {"shout": shout} @@ -123,18 +124,17 @@ async def update_shout(_, info, inp): @mutation.field("deleteShout") @login_required async def delete_shout(_, info, slug): - auth = info.context["request"].auth - user_id = auth.user_id + auth: AuthCredentials = info.context["request"].auth with local_session() as session: shout = session.query(Shout).filter(Shout.slug == slug).first() authors = [a.id for a in shout.authors] if not shout: return {"error": "invalid shout slug"} - if user_id not in authors: + if auth.user_id not in authors: return {"error": "access denied"} for a in authors: - reactions_unfollow(a.slug, slug, True) + reactions_unfollow(a.id, slug) shout.deletedAt = datetime.now(tz=timezone.utc) session.add(shout) session.commit() diff --git a/resolvers/inbox/chats.py b/resolvers/inbox/chats.py index 42466890..c373c6ca 100644 --- a/resolvers/inbox/chats.py +++ b/resolvers/inbox/chats.py @@ -3,6 +3,7 @@ import uuid from datetime import datetime, timezone from auth.authenticate import login_required +from auth.credentials import AuthCredentials from base.redis import redis from base.resolvers import mutation @@ -18,7 +19,7 @@ async def update_chat(_, info, chat_new: dict): :param chat_new: dict with chat data :return: Result { error chat } """ - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth chat_id = chat_new["id"] chat = await redis.execute("GET", f"chats/{chat_id}") if not chat: @@ -26,7 +27,9 @@ async def update_chat(_, info, chat_new: dict): "error": "chat not exist" } chat = dict(json.loads(chat)) - if user.slug in chat["admins"]: + + # TODO + if auth.user_id in chat["admins"]: chat.update({ "title": chat_new.get("title", chat["title"]), "description": chat_new.get("description", chat["description"]), @@ -46,10 +49,11 @@ async def update_chat(_, info, chat_new: dict): @mutation.field("createChat") @login_required async def create_chat(_, info, title="", members=[]): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth chat = {} - if user.slug not in members: - members.append(user.slug) + + if auth.user_id not in members: + members.append(auth.user_id) # reuse chat craeted before if exists if len(members) == 2 and title == "": @@ -73,7 +77,7 @@ async def create_chat(_, info, title="", members=[]): "id": chat_id, "users": members, "title": title, - "createdBy": user.slug, + "createdBy": auth.user_id, "createdAt": int(datetime.now(tz=timezone.utc).timestamp()), "updatedAt": int(datetime.now(tz=timezone.utc).timestamp()), "admins": [] @@ -93,13 +97,14 @@ async def create_chat(_, info, title="", members=[]): @mutation.field("deleteChat") @login_required async def delete_chat(_, info, chat_id: str): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth + chat = await redis.execute("GET", f"/chats/{chat_id}") if chat: chat = dict(json.loads(chat)) - if user.slug in chat['admins']: + if auth.user_id in chat['admins']: await redis.execute("DEL", f"chats/{chat_id}") - await redis.execute("SREM", "chats_by_user/" + user, chat_id) + await redis.execute("SREM", "chats_by_user/" + str(auth.user_id), chat_id) await redis.execute("COMMIT") else: return { diff --git a/resolvers/inbox/load.py b/resolvers/inbox/load.py index 313da074..fa32e4f0 100644 --- a/resolvers/inbox/load.py +++ b/resolvers/inbox/load.py @@ -2,6 +2,7 @@ import json from datetime import datetime, timedelta, timezone from auth.authenticate import login_required +from auth.credentials import AuthCredentials from base.redis import redis from base.orm import local_session from base.resolvers import query @@ -30,12 +31,9 @@ async def load_messages(chat_id: str, limit: int, offset: int): @login_required async def load_chats(_, info, limit: int = 50, offset: int = 0): """ load :limit chats of current user with :offset """ - user = info.context["request"].user - if user: - print('[inbox] load user\'s chats %s' % user.slug) - else: - raise Unauthorized("Please login to load chats") - cids = await redis.execute("SMEMBERS", "chats_by_user/" + user.slug) + auth: AuthCredentials = info.context["request"].auth + + cids = await redis.execute("SMEMBERS", "chats_by_user/" + str(auth.user_id)) if cids: cids = list(cids)[offset:offset + limit] if not cids: @@ -47,7 +45,7 @@ async def load_chats(_, info, limit: int = 50, offset: int = 0): if c: c = dict(json.loads(c)) c['messages'] = await load_messages(cid, 5, 0) - c['unread'] = await get_unread_counter(cid, user.slug) + c['unread'] = await get_unread_counter(cid, auth.user_id) with local_session() as session: c['members'] = [] for userslug in c["users"]: @@ -65,11 +63,11 @@ async def load_chats(_, info, limit: int = 50, offset: int = 0): } -async def search_user_chats(by, messages: set, slug: str, limit, offset): +async def search_user_chats(by, messages: set, user_id: int, limit, offset): cids = set([]) by_author = by.get('author') body_like = by.get('body') - cids.unioin(set(await redis.execute("SMEMBERS", "chats_by_user/" + slug))) + cids.unioin(set(await redis.execute("SMEMBERS", "chats_by_user/" + str(user_id)))) if by_author: # all author's messages cids.union(set(await redis.execute("SMEMBERS", f"chats_by_user/{by_author}"))) @@ -104,9 +102,11 @@ async def load_messages_by(_, info, by, limit: int = 10, offset: int = 0): # everyone's messages in filtered chat messages.union(set(await load_messages(by_chat, limit, offset))) - user = info.context["request"].user - if user and len(messages) == 0: - messages.union(search_user_chats(by, messages, user.slug, limit, offset)) + auth: AuthCredentials = info.context["request"].auth + + if len(messages) == 0: + # FIXME + messages.union(search_user_chats(by, messages, auth.user_id, limit, offset)) days = by.get("days") if days: @@ -126,9 +126,10 @@ async def load_messages_by(_, info, by, limit: int = 10, offset: int = 0): @query.field("loadRecipients") async def load_recipients(_, info, limit=50, offset=0): chat_users = [] - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth + try: - chat_users += await followed_authors(user.slug) + chat_users += await followed_authors(auth.user_id) limit = limit - len(chat_users) except Exception: pass diff --git a/resolvers/inbox/messages.py b/resolvers/inbox/messages.py index 84734a61..2772dcf6 100644 --- a/resolvers/inbox/messages.py +++ b/resolvers/inbox/messages.py @@ -3,6 +3,7 @@ import json from datetime import datetime, timezone from auth.authenticate import login_required +from auth.credentials import AuthCredentials from base.redis import redis from base.resolvers import mutation, subscription from services.inbox import ChatFollowing, MessageResult, MessagesStorage @@ -12,7 +13,8 @@ from services.inbox import ChatFollowing, MessageResult, MessagesStorage @login_required async def create_message(_, info, chat: str, body: str, replyTo=None): """ create message with :body for :chat_id replying to :replyTo optionally """ - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth + chat = await redis.execute("GET", f"chats/{chat}") if not chat: return { @@ -25,7 +27,7 @@ async def create_message(_, info, chat: str, body: str, replyTo=None): new_message = { "chatId": chat['id'], "id": message_id, - "author": user.slug, + "author": auth.user_id, "body": body, "replyTo": replyTo, "createdAt": int(datetime.now(tz=timezone.utc).timestamp()), @@ -55,7 +57,7 @@ async def create_message(_, info, chat: str, body: str, replyTo=None): @mutation.field("updateMessage") @login_required async def update_message(_, info, chat_id: str, message_id: int, body: str): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth chat = await redis.execute("GET", f"chats/{chat_id}") if not chat: @@ -66,7 +68,7 @@ async def update_message(_, info, chat_id: str, message_id: int, body: str): return {"error": "message not exist"} message = json.loads(message) - if message["author"] != user.slug: + if message["author"] != auth.user_id: return {"error": "access denied"} message["body"] = body @@ -86,7 +88,7 @@ async def update_message(_, info, chat_id: str, message_id: int, body: str): @mutation.field("deleteMessage") @login_required async def delete_message(_, info, chat_id: str, message_id: int): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth chat = await redis.execute("GET", f"chats/{chat_id}") if not chat: @@ -97,15 +99,15 @@ async def delete_message(_, info, chat_id: str, message_id: int): if not message: return {"error": "message not exist"} message = json.loads(message) - if message["author"] != user.slug: + if message["author"] != auth.user_id: return {"error": "access denied"} await redis.execute("LREM", f"chats/{chat_id}/message_ids", 0, str(message_id)) await redis.execute("DEL", f"chats/{chat_id}/messages/{str(message_id)}") users = chat["users"] - for user_slug in users: - await redis.execute("LREM", f"chats/{chat_id}/unread/{user_slug}", 0, str(message_id)) + for user_id in users: + await redis.execute("LREM", f"chats/{chat_id}/unread/{user_id}", 0, str(message_id)) result = MessageResult("DELETED", message) await MessagesStorage.put(result) @@ -116,7 +118,7 @@ async def delete_message(_, info, chat_id: str, message_id: int): @mutation.field("markAsRead") @login_required async def mark_as_read(_, info, chat_id: str, messages: [int]): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth chat = await redis.execute("GET", f"chats/{chat_id}") if not chat: @@ -124,11 +126,11 @@ async def mark_as_read(_, info, chat_id: str, messages: [int]): chat = json.loads(chat) users = set(chat["users"]) - if user.slug not in users: + if auth.user_id not in users: return {"error": "access denied"} for message_id in messages: - await redis.execute("LREM", f"chats/{chat_id}/unread/{user.slug}", 0, str(message_id)) + await redis.execute("LREM", f"chats/{chat_id}/unread/{auth.user_id}", 0, str(message_id)) return { "error": None @@ -139,8 +141,9 @@ async def mark_as_read(_, info, chat_id: str, messages: [int]): @login_required async def message_generator(obj, info): try: - user = info.context["request"].user - user_following_chats = await redis.execute("GET", f"chats_by_user/{user.slug}") + auth: AuthCredentials = info.context["request"].auth + + user_following_chats = await redis.execute("GET", f"chats_by_user/{auth.user_id}") if user_following_chats: user_following_chats = list(json.loads(user_following_chats)) # chat ids else: diff --git a/resolvers/inbox/search.py b/resolvers/inbox/search.py index 5a1289ac..20269e59 100644 --- a/resolvers/inbox/search.py +++ b/resolvers/inbox/search.py @@ -1,6 +1,7 @@ import json from auth.authenticate import login_required +from auth.credentials import AuthCredentials from base.redis import redis from base.resolvers import query from base.orm import local_session @@ -12,8 +13,8 @@ from orm.user import AuthorFollower, User async def search_recipients(_, info, query: str, limit: int = 50, offset: int = 0): result = [] # TODO: maybe redis scan? - user = info.context["request"].user - talk_before = await redis.execute("GET", f"/chats_by_user/{user.slug}") + auth: AuthCredentials = info.context["request"].auth + talk_before = await redis.execute("GET", f"/chats_by_user/{auth.user_id}") if talk_before: talk_before = list(json.loads(talk_before))[offset:offset + limit] for chat_id in talk_before: @@ -24,7 +25,6 @@ async def search_recipients(_, info, query: str, limit: int = 50, offset: int = if member.startswith(query): if member not in result: result.append(member) - user = info.context["request"].user more_amount = limit - len(result) diff --git a/resolvers/zine/following.py b/resolvers/zine/following.py index d747156c..753d5e67 100644 --- a/resolvers/zine/following.py +++ b/resolvers/zine/following.py @@ -1,4 +1,5 @@ from auth.authenticate import login_required +from auth.credentials import AuthCredentials from base.resolvers import mutation # from resolvers.community import community_follow, community_unfollow from resolvers.zine.profile import author_follow, author_unfollow @@ -9,17 +10,18 @@ from resolvers.zine.topics import topic_follow, topic_unfollow @mutation.field("follow") @login_required async def follow(_, info, what, slug): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth + try: if what == "AUTHOR": - author_follow(user, slug) + author_follow(auth.user_id, slug) elif what == "TOPIC": - topic_follow(user, slug) + topic_follow(auth.user_id, slug) elif what == "COMMUNITY": # community_follow(user, slug) pass elif what == "REACTIONS": - reactions_follow(user, slug) + reactions_follow(auth.user_id, slug) except Exception as e: return {"error": str(e)} @@ -29,18 +31,18 @@ async def follow(_, info, what, slug): @mutation.field("unfollow") @login_required async def unfollow(_, info, what, slug): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth try: if what == "AUTHOR": - author_unfollow(user, slug) + author_unfollow(auth.user_id, slug) elif what == "TOPIC": - topic_unfollow(user, slug) + topic_unfollow(auth.user_id, slug) elif what == "COMMUNITY": # community_unfollow(user, slug) pass elif what == "REACTIONS": - reactions_unfollow(user, slug) + reactions_unfollow(auth.user_id, slug) except Exception as e: return {"error": str(e)} diff --git a/resolvers/zine/load.py b/resolvers/zine/load.py index 63896348..34ff0ead 100644 --- a/resolvers/zine/load.py +++ b/resolvers/zine/load.py @@ -1,6 +1,8 @@ from datetime import datetime, timedelta, timezone from sqlalchemy.orm import joinedload, aliased from sqlalchemy.sql.expression import desc, asc, select, func + +from auth.credentials import AuthCredentials from base.orm import local_session from base.resolvers import query from orm import ViewedEntry @@ -15,10 +17,10 @@ def add_stat_columns(q): return add_common_stat_columns(q) -def apply_filters(q, filters, user=None): +def apply_filters(q, filters, user_id=None): - if filters.get("reacted") and user: - q.join(Reaction, Reaction.createdBy == user.id) + if filters.get("reacted") and user_id: + q.join(Reaction, Reaction.createdBy == user_id) v = filters.get("visibility") if v == "public": @@ -105,17 +107,15 @@ async def load_shouts_by(_, info, options): q = add_stat_columns(q) - user = info.context["request"].user - q = apply_filters(q, options.get("filters", {}), user) + auth: AuthCredentials = info.context["request"].auth + q = apply_filters(q, options.get("filters", {}), auth.user_id) order_by = options.get("order_by", Shout.createdAt) if order_by == 'reacted': aliased_reaction = aliased(Reaction) q.outerjoin(aliased_reaction).add_columns(func.max(aliased_reaction.createdAt).label('reacted')) - order_by_desc = options.get('order_by_desc', True) - - query_order_by = desc(order_by) if order_by_desc else asc(order_by) + query_order_by = desc(order_by) if options.get('order_by_desc', True) else asc(order_by) offset = options.get("offset", 0) limit = options.get("limit", 10) diff --git a/resolvers/zine/profile.py b/resolvers/zine/profile.py index 3cc142e7..0944eb3f 100644 --- a/resolvers/zine/profile.py +++ b/resolvers/zine/profile.py @@ -4,6 +4,7 @@ from sqlalchemy import and_, func, distinct, select, literal from sqlalchemy.orm import aliased, joinedload from auth.authenticate import login_required +from auth.credentials import AuthCredentials from base.orm import local_session from base.resolvers import mutation, query from orm.reaction import Reaction @@ -40,11 +41,10 @@ def add_author_stat_columns(q): # func.sum(user_rating_aliased.value).label('rating_stat') # ) - # q = q.add_columns(literal(0).label('commented_stat')) - - q = q.outerjoin(Reaction, and_(Reaction.createdBy == User.id, Reaction.body.is_not(None))).add_columns( - func.count(distinct(Reaction.id)).label('commented_stat') - ) + q = q.add_columns(literal(0).label('commented_stat')) + # q = q.outerjoin(Reaction, and_(Reaction.createdBy == User.id, Reaction.body.is_not(None))).add_columns( + # func.count(distinct(Reaction.id)).label('commented_stat') + # ) q = q.group_by(User.id) @@ -74,25 +74,25 @@ def get_authors_from_query(q): return authors -async def user_subscriptions(slug: str): +async def user_subscriptions(user_id: int): return { - "unread": await get_total_unread_counter(slug), # unread inbox messages counter - "topics": [t.slug for t in await followed_topics(slug)], # followed topics slugs - "authors": [a.slug for a in await followed_authors(slug)], # followed authors slugs - "reactions": await followed_reactions(slug) + "unread": await get_total_unread_counter(user_id), # unread inbox messages counter + "topics": [t.slug for t in await followed_topics(user_id)], # followed topics slugs + "authors": [a.slug for a in await followed_authors(user_id)], # followed authors slugs + "reactions": await followed_reactions(user_id) # "communities": [c.slug for c in followed_communities(slug)], # communities } # @query.field("userFollowedDiscussions") -@login_required -async def followed_discussions(_, info, slug) -> List[Topic]: - return await followed_reactions(slug) +# @login_required +async def followed_discussions(_, info, user_id) -> List[Topic]: + return await followed_reactions(user_id) -async def followed_reactions(slug): +async def followed_reactions(user_id): with local_session() as session: - user = session.query(User).where(User.slug == slug).first() + user = session.query(User).where(User.id == user_id).first() return session.query( Reaction.shout ).where( @@ -104,31 +104,26 @@ async def followed_reactions(slug): @query.field("userFollowedTopics") @login_required -async def get_followed_topics(_, info, slug) -> List[Topic]: - return await followed_topics(slug) +async def get_followed_topics(_, info, user_id) -> List[Topic]: + return await followed_topics(user_id) -async def followed_topics(slug): - return followed_by_user(slug) +async def followed_topics(user_id): + return followed_by_user(user_id) @query.field("userFollowedAuthors") -async def get_followed_authors(_, _info, slug) -> List[User]: - return await followed_authors(slug) +async def get_followed_authors(_, _info, user_id: int) -> List[User]: + return await followed_authors(user_id) -async def followed_authors(slug): - with local_session() as session: - user = session.query(User).where(User.slug == slug).first() - q = select(User) - q = add_author_stat_columns(q) - aliased_user = aliased(User) - q = q.join(AuthorFollower, AuthorFollower.author == user.id).join( - aliased_user, aliased_user.id == AuthorFollower.follower - ).where( - aliased_user.slug == slug - ) - return get_authors_from_query(q) +async def followed_authors(user_id): + q = select(User) + q = add_author_stat_columns(q) + q = q.join(AuthorFollower, AuthorFollower.author == User.id).where( + AuthorFollower.follower == user_id + ) + return get_authors_from_query(q) @query.field("userFollowers") @@ -157,25 +152,16 @@ async def get_user_roles(slug): .all() ) - return roles + return [] # roles @mutation.field("updateProfile") @login_required async def update_profile(_, info, profile): - print('[zine] update_profile') - print(profile) auth = info.context["request"].auth user_id = auth.user_id with local_session() as session: - session.query(User).filter(User.id == user_id).update({ - "name": profile['name'], - "slug": profile['slug'], - "bio": profile['bio'], - "userpic": profile['userpic'], - "about": profile['about'], - "links": profile['links'] - }) + session.query(User).filter(User.id == user_id).update(profile) session.commit() return {} @@ -183,11 +169,12 @@ async def update_profile(_, info, profile): @mutation.field("rateUser") @login_required async def rate_user(_, info, rated_userslug, value): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth + with local_session() as session: rating = ( session.query(UserRating) - .filter(and_(UserRating.rater == user.slug, UserRating.user == rated_userslug)) + .filter(and_(UserRating.rater == auth.user_id, UserRating.user == rated_userslug)) .first() ) if rating: @@ -195,30 +182,30 @@ async def rate_user(_, info, rated_userslug, value): session.commit() return {} try: - UserRating.create(rater=user.slug, user=rated_userslug, value=value) + UserRating.create(rater=auth.user_id, user=rated_userslug, value=value) except Exception as err: return {"error": err} return {} # for mutation.field("follow") -def author_follow(user, slug): +def author_follow(user_id, slug): with local_session() as session: author = session.query(User).where(User.slug == slug).one() - af = AuthorFollower.create(follower=user.id, author=author.id) + af = AuthorFollower.create(follower=user_id, author=author.id) session.add(af) session.commit() # for mutation.field("unfollow") -def author_unfollow(user, slug): +def author_unfollow(user_id, slug): with local_session() as session: flw = ( session.query( AuthorFollower ).join(User, User.id == AuthorFollower.author).filter( and_( - AuthorFollower.follower == user.id, User.slug == slug + AuthorFollower.follower == user_id, User.slug == slug ) ).first() ) diff --git a/resolvers/zine/reactions.py b/resolvers/zine/reactions.py index c0648fcd..527dde9d 100644 --- a/resolvers/zine/reactions.py +++ b/resolvers/zine/reactions.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta, timezone from sqlalchemy import and_, asc, desc, select, text, func from sqlalchemy.orm import aliased from auth.authenticate import login_required +from auth.credentials import AuthCredentials from base.orm import local_session from base.resolvers import mutation, query from orm.reaction import Reaction, ReactionKind @@ -14,20 +15,20 @@ def add_reaction_stat_columns(q): return add_common_stat_columns(q) -def reactions_follow(user: User, slug: str, auto=False): +def reactions_follow(user_id, slug: str, auto=False): with local_session() as session: shout = session.query(Shout).where(Shout.slug == slug).one() following = ( session.query(ShoutReactionsFollower).where(and_( - ShoutReactionsFollower.follower == user.id, + ShoutReactionsFollower.follower == user_id, ShoutReactionsFollower.shout == shout.id, )).first() ) if not following: following = ShoutReactionsFollower.create( - follower=user.id, + follower=user_id, shout=shout.id, auto=auto ) @@ -35,13 +36,13 @@ def reactions_follow(user: User, slug: str, auto=False): session.commit() -def reactions_unfollow(user, slug): +def reactions_unfollow(user_id, slug): with local_session() as session: shout = session.query(Shout).where(Shout.slug == slug).one() following = ( session.query(ShoutReactionsFollower).where(and_( - ShoutReactionsFollower.follower == user.id, + ShoutReactionsFollower.follower == user_id, ShoutReactionsFollower.shout == shout.id )).first() ) @@ -51,12 +52,12 @@ def reactions_unfollow(user, slug): session.commit() -def is_published_author(session, userslug): +def is_published_author(session, user_id): ''' checks if user has at least one publication ''' return session.query( Shout ).where( - Shout.authors.contains(userslug) + Shout.authors.contains(user_id) ).filter( and_( Shout.publishedAt.is_not(None), @@ -65,17 +66,17 @@ def is_published_author(session, userslug): ).count() > 0 -def check_to_publish(session, user, reaction): +def check_to_publish(session, user_id, reaction): ''' set shout to public if publicated approvers amount > 4 ''' if not reaction.replyTo and reaction.kind in [ ReactionKind.ACCEPT, ReactionKind.LIKE, ReactionKind.PROOF ]: - if is_published_author(user): + if is_published_author(user_id): # now count how many approvers are voted already approvers_reactions = session.query(Reaction).where(Reaction.shout == reaction.shout).all() - approvers = [user.slug, ] + approvers = [user_id, ] for ar in approvers_reactions: a = ar.createdBy if is_published_author(session, a): @@ -85,7 +86,7 @@ def check_to_publish(session, user, reaction): return False -def check_to_hide(session, user, reaction): +def check_to_hide(session, user_id, reaction): ''' hides any shout if 20% of reactions are negative ''' if not reaction.replyTo and reaction.kind in [ ReactionKind.DECLINE, @@ -107,8 +108,8 @@ def check_to_hide(session, user, reaction): return False -def set_published(session, slug, publisher): - s = session.query(Shout).where(Shout.slug == slug).first() +def set_published(session, shout_id, publisher): + s = session.query(Shout).where(Shout.id == shout_id).first() s.publishedAt = datetime.now(tz=timezone.utc) s.publishedBy = publisher s.visibility = text('public') @@ -116,8 +117,8 @@ def set_published(session, slug, publisher): session.commit() -def set_hidden(session, slug): - s = session.query(Shout).where(Shout.slug == slug).first() +def set_hidden(session, shout_id): + s = session.query(Shout).where(Shout.id == shout_id).first() s.visibility = text('authors') s.publishedAt = None # TODO: discuss s.publishedBy = None # TODO: store changes history in git @@ -128,7 +129,7 @@ def set_hidden(session, slug): @mutation.field("createReaction") @login_required async def create_reaction(_, info, inp): - user = info.context["request"].user + auth: AuthCredentials = info.context["request"].auth with local_session() as session: reaction = Reaction.create(**inp) @@ -137,13 +138,13 @@ async def create_reaction(_, info, inp): # self-regulation mechanics - if check_to_hide(session, user, reaction): + if check_to_hide(session, auth.user_id, reaction): set_hidden(session, reaction.shout) - elif check_to_publish(session, user, reaction): + elif check_to_publish(session, auth.user_id, reaction): set_published(session, reaction.shout, reaction.createdBy) try: - reactions_follow(user, inp["shout"], True) + reactions_follow(auth.user_id, inp["shout"], True) except Exception as e: print(f"[resolvers.reactions] error on reactions autofollowing: {e}") @@ -158,11 +159,10 @@ async def create_reaction(_, info, inp): @mutation.field("updateReaction") @login_required async def update_reaction(_, info, inp): - auth = info.context["request"].auth - user_id = auth.user_id + auth: AuthCredentials = info.context["request"].auth with local_session() as session: - user = session.query(User).where(User.id == user_id).first() + user = session.query(User).where(User.id == auth.user_id).first() q = select(Reaction).filter(Reaction.id == inp.id) q = add_reaction_stat_columns(q) @@ -193,10 +193,10 @@ async def update_reaction(_, info, inp): @mutation.field("deleteReaction") @login_required async def delete_reaction(_, info, rid): - auth = info.context["request"].auth - user_id = auth.user_id + auth: AuthCredentials = info.context["request"].auth + with local_session() as session: - user = session.query(User).where(User.id == user_id).first() + user = session.query(User).where(User.id == auth.user_id).first() reaction = session.query(Reaction).filter(Reaction.id == rid).first() if not reaction: return {"error": "invalid reaction id"} diff --git a/resolvers/zine/topics.py b/resolvers/zine/topics.py index 2ee6809b..ad4f8c77 100644 --- a/resolvers/zine/topics.py +++ b/resolvers/zine/topics.py @@ -1,4 +1,6 @@ from sqlalchemy import and_, select, distinct, func +from sqlalchemy.orm import aliased + from auth.authenticate import login_required from base.orm import local_session from base.resolvers import mutation, query @@ -8,16 +10,20 @@ from orm import Shout, User def add_topic_stat_columns(q): - q = q.outerjoin(ShoutTopic, Topic.id == ShoutTopic.topic).add_columns( - func.count(distinct(ShoutTopic.shout)).label('shouts_stat') - ).outerjoin(ShoutAuthor, ShoutTopic.shout == ShoutAuthor.shout).add_columns( - func.count(distinct(ShoutAuthor.user)).label('authors_stat') - ).outerjoin(TopicFollower, + aliased_shout_topic = aliased(ShoutTopic) + aliased_shout_author = aliased(ShoutAuthor) + aliased_topic_follower = aliased(TopicFollower) + + q = q.outerjoin(aliased_shout_topic, Topic.id == aliased_shout_topic.topic).add_columns( + func.count(distinct(aliased_shout_topic.shout)).label('shouts_stat') + ).outerjoin(aliased_shout_author, aliased_shout_topic.shout == aliased_shout_author.shout).add_columns( + func.count(distinct(aliased_shout_author.user)).label('authors_stat') + ).outerjoin(aliased_topic_follower, and_( - TopicFollower.topic == Topic.id, - TopicFollower.follower == ShoutAuthor.id + aliased_topic_follower.topic == Topic.id, + aliased_topic_follower.follower == aliased_shout_author.id )).add_columns( - func.count(distinct(TopicFollower.follower)).label('followers_stat') + func.count(distinct(aliased_topic_follower.follower)).label('followers_stat') ) q = q.group_by(Topic.id) @@ -46,10 +52,10 @@ def get_topics_from_query(q): return topics -def followed_by_user(user_slug): +def followed_by_user(user_id): q = select(Topic) q = add_topic_stat_columns(q) - q = q.join(User).where(User.slug == user_slug) + q = q.join(TopicFollower).where(TopicFollower.follower == user_id) return get_topics_from_query(q) @@ -115,21 +121,21 @@ async def update_topic(_, _info, inp): return {"topic": topic} -async def topic_follow(user, slug): +def topic_follow(user_id, slug): with local_session() as session: topic = session.query(Topic).where(Topic.slug == slug).one() - following = TopicFollower.create(topic=topic.id, follower=user.id) + following = TopicFollower.create(topic=topic.id, follower=user_id) session.add(following) session.commit() -async def topic_unfollow(user, slug): +def topic_unfollow(user_id, slug): with local_session() as session: sub = ( session.query(TopicFollower).join(Topic).filter( and_( - TopicFollower.follower == user.id, + TopicFollower.follower == user_id, Topic.slug == slug ) ).first() @@ -145,7 +151,7 @@ async def topic_unfollow(user, slug): async def topics_random(_, info, amount=12): q = select(Topic) q = add_topic_stat_columns(q) - q = q.join(Shout, ShoutTopic.shout == Shout.id).group_by(Topic.id).having(func.count(Shout.id) > 2) + q = q.join(ShoutTopic).join(Shout, ShoutTopic.shout == Shout.id).group_by(Topic.id).having(func.count(Shout.id) > 2) q = q.order_by(func.random()).limit(amount) return get_topics_from_query(q)