From 27acf62c2e0be1ac189e4fbb6a3ee0d5c86c2935 Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Mon, 6 Feb 2023 16:09:26 +0300 Subject: [PATCH 1/9] init-following-manager --- resolvers/inbox/messages.py | 21 +++++----- resolvers/zine/following.py | 80 +++++++++++++++++++++++++++++++++---- services/following.py | 48 ++++++++++++++++++++++ services/inbox/helpers.py | 14 ------- services/inbox/storage.py | 23 ----------- 5 files changed, 130 insertions(+), 56 deletions(-) create mode 100644 services/following.py delete mode 100644 services/inbox/helpers.py delete mode 100644 services/inbox/storage.py diff --git a/resolvers/inbox/messages.py b/resolvers/inbox/messages.py index 3a6936d0..d33af9c3 100644 --- a/resolvers/inbox/messages.py +++ b/resolvers/inbox/messages.py @@ -7,8 +7,7 @@ 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.helpers import ChatFollowing, MessageResult -from services.inbox.storage import MessagesStorage +from services.following import FollowingManager, FollowingResult, Following from validations.inbox import Message @@ -51,8 +50,8 @@ async def create_message(_, info, chat: str, body: str, replyTo=None): "LPUSH", f"chats/{chat['id']}/unread/{user_slug}", str(message_id) ) - result = MessageResult("NEW", new_message) - await MessagesStorage.put(result) + result = FollowingResult("NEW", 'chat', new_message) + await FollowingManager.put('chat', result) return { "message": new_message, @@ -82,8 +81,8 @@ async def update_message(_, info, chat_id: str, message_id: int, body: str): await redis.execute("SET", f"chats/{chat_id}/messages/{message_id}", json.dumps(message)) - result = MessageResult("UPDATED", message) - await MessagesStorage.put(result) + result = FollowingResult("UPDATED", 'chat', message) + await FollowingManager.put('chat', result) return { "message": message, @@ -115,8 +114,8 @@ async def delete_message(_, info, chat_id: str, message_id: int): 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) + result = FollowingResult("DELETED", 'chat', message) + await FollowingManager.put(result) return {} @@ -162,8 +161,8 @@ async def message_generator(_, info: GraphQLResolveInfo): user_following_chats_sorted = sorted(user_following_chats, key=lambda x: updated[x], reverse=True) for chat_id in user_following_chats_sorted: - following_chat = ChatFollowing(chat_id) - await MessagesStorage.register_chat(following_chat) + following_chat = Following('chat', chat_id) + await FollowingManager.register('chat', following_chat) chat_task = following_chat.queue.get() tasks.append(chat_task) @@ -171,7 +170,7 @@ async def message_generator(_, info: GraphQLResolveInfo): msg = await asyncio.gather(*tasks) yield msg finally: - await MessagesStorage.remove_chat(following_chat) + await FollowingManager.remove('chat', following_chat) @subscription.field("newMessage") diff --git a/resolvers/zine/following.py b/resolvers/zine/following.py index f6acff43..72c3a0e9 100644 --- a/resolvers/zine/following.py +++ b/resolvers/zine/following.py @@ -1,14 +1,35 @@ +import asyncio +from base.orm import local_session +from base.resolvers import mutation, subscription, query from auth.authenticate import login_required from auth.credentials import AuthCredentials -from base.resolvers import mutation, subscription # from resolvers.community import community_follow, community_unfollow +from orm.user import AuthorFollower +from orm.topic import TopicFollower +from orm.shout import Shout, ShoutReactionsFollower from resolvers.zine.profile import author_follow, author_unfollow from resolvers.zine.reactions import reactions_follow, reactions_unfollow from resolvers.zine.topics import topic_follow, topic_unfollow -import asyncio +from services.following import Following, FollowingManager, FollowingResult from graphql.type import GraphQLResolveInfo +@query.field("myFeed") +@login_required +async def get_my_feed(_, info): + auth: AuthCredentials = info.context["request"].auth + user_id = auth.user_id + try: + with local_session() as session: + following_authors = session.query(AuthorFollower).where(AuthorFollower.follower == user_id).unique().all() + following_topics = session.query(TopicFollower).where(TopicFollower.follower == user_id).unique().all() + # TODO: my feed query + shouts = [] + return shouts + except Exception: + pass + + @mutation.field("follow") @login_required async def follow(_, info, what, slug): @@ -17,13 +38,21 @@ async def follow(_, info, what, slug): try: if what == "AUTHOR": author_follow(auth.user_id, slug) + result = FollowingResult("NEW", 'author', slug) + await FollowingManager.put('author', result) elif what == "TOPIC": topic_follow(auth.user_id, slug) + result = FollowingResult("NEW", 'topic', slug) + await FollowingManager.put('topic', result) elif what == "COMMUNITY": # community_follow(user, slug) + # result = FollowingResult("NEW", 'community', slug) + # await FollowingManager.put('community', result) pass elif what == "REACTIONS": reactions_follow(auth.user_id, slug) + result = FollowingResult("NEW", 'shout', slug) + await FollowingManager.put('shout', result) except Exception as e: return {"error": str(e)} @@ -38,19 +67,28 @@ async def unfollow(_, info, what, slug): try: if what == "AUTHOR": author_unfollow(auth.user_id, slug) + result = FollowingResult("DELETED", 'author', slug) + await FollowingManager.put('author', result) elif what == "TOPIC": topic_unfollow(auth.user_id, slug) + result = FollowingResult("DELETED", 'topic', slug) + await FollowingManager.put('topic', result) elif what == "COMMUNITY": # community_unfollow(user, slug) + # result = FollowingResult("DELETED", 'community', slug) + # await FollowingManager.put('community', result) pass elif what == "REACTIONS": reactions_unfollow(auth.user_id, slug) + result = FollowingResult("DELETED", 'shout', slug) + await FollowingManager.put('shout', result) except Exception as e: return {"error": str(e)} return {} +# by author and by topic @subscription.source("newShout") @login_required async def shout_generator(_, info: GraphQLResolveInfo): @@ -60,7 +98,24 @@ async def shout_generator(_, info: GraphQLResolveInfo): try: tasks = [] - # TODO: implement when noticing new shout + with local_session() as session: + following_authors = session.query(AuthorFollower).where( + AuthorFollower.follower == user_id).all() + following_topics = session.query(TopicFollower).where(TopicFollower.follower == user_id).all() + + # notify new shout + + for topic_id in following_topics: + following_topic = Following('topic', topic_id) + await FollowingManager.register('topic', following_topic) + following_topic_task = following_topic.queue.get() + tasks.append(following_topic_task) + + for author_id in following_authors: + following_author = Following('author', author_id) + await FollowingManager.register('author', following_author) + following_author_task = following_author.queue.get() + tasks.append(following_author_task) while True: shout = await asyncio.gather(*tasks) @@ -76,12 +131,21 @@ async def reaction_generator(_, info): auth: AuthCredentials = info.context["request"].auth user_id = auth.user_id try: - tasks = [] + with local_session() as session: + followings = session.query(ShoutReactionsFollower.shout).where( + ShoutReactionsFollower.follower == user_id).unique() - # TODO: implement when noticing new reaction + # notify new reaction - while True: - reaction = await asyncio.gather(*tasks) - yield reaction + tasks = [] + for shout_id in followings: + following_shout = Following('shout', shout_id) + await FollowingManager.register('shout', following_shout) + following_author_task = following_shout.queue.get() + tasks.append(following_author_task) + + while True: + reaction = await asyncio.gather(*tasks) + yield reaction finally: pass diff --git a/services/following.py b/services/following.py new file mode 100644 index 00000000..f67f78a2 --- /dev/null +++ b/services/following.py @@ -0,0 +1,48 @@ +import asyncio + + +class FollowingResult: + def __init__(self, event, kind, payload): + self.event = event + self.kind = kind + self.payload = payload + + +class Following: + queue = asyncio.Queue() + + def __init__(self, kind, uid): + self.kind = kind # author topic shout chat + self.uid = uid + + +class FollowingManager: + lock = asyncio.Lock() + data = { + 'author': [], + 'topic': [], + 'shout': [], + 'chat': [] + } + + @staticmethod + async def register(kind, uid): + async with FollowingManager.lock: + FollowingManager[kind].append(uid) + + @staticmethod + async def remove(kind, uid): + async with FollowingManager.lock: + FollowingManager[kind].remove(uid) + + @staticmethod + async def push(kind, payload): + async with FollowingManager.lock: + if kind == 'chat': + for chat in FollowingManager['chat']: + if payload.message["chatId"] == chat.uid: + chat.queue.put_nowait(payload) + else: + for entity in FollowingManager[kind]: + if payload.shout['createdBy'] == entity.uid: + entity.queue.put_nowait(payload) diff --git a/services/inbox/helpers.py b/services/inbox/helpers.py deleted file mode 100644 index d8791218..00000000 --- a/services/inbox/helpers.py +++ /dev/null @@ -1,14 +0,0 @@ -import asyncio - - -class MessageResult: - def __init__(self, status, message): - self.seen = status - self.message = message - - -class ChatFollowing: - queue = asyncio.Queue() - - def __init__(self, chat_id): - self.chat_id = chat_id diff --git a/services/inbox/storage.py b/services/inbox/storage.py deleted file mode 100644 index dd6e5fcf..00000000 --- a/services/inbox/storage.py +++ /dev/null @@ -1,23 +0,0 @@ -import asyncio - - -class MessagesStorage: - lock = asyncio.Lock() - chats = [] - - @staticmethod - async def register_chat(chat): - async with MessagesStorage.lock: - MessagesStorage.chats.append(chat) - - @staticmethod - async def remove_chat(chat): - async with MessagesStorage.lock: - MessagesStorage.chats.remove(chat) - - @staticmethod - async def put(message_result): - async with MessagesStorage.lock: - for chat in MessagesStorage.chats: - if message_result.message["chatId"] == chat.chat_id: - chat.queue.put_nowait(message_result) From 3a678a73436de2083a4fe9748d37ae6eee4f06bf Mon Sep 17 00:00:00 2001 From: bniwredyc Date: Mon, 6 Feb 2023 15:27:23 +0100 Subject: [PATCH 2/9] my feed query --- resolvers/zine/load.py | 46 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/resolvers/zine/load.py b/resolvers/zine/load.py index 3bf3bbe2..0dfc9468 100644 --- a/resolvers/zine/load.py +++ b/resolvers/zine/load.py @@ -3,13 +3,15 @@ from datetime import datetime, timedelta, timezone from sqlalchemy.orm import joinedload, aliased from sqlalchemy.sql.expression import desc, asc, select, func, case +from auth.authenticate import login_required from auth.credentials import AuthCredentials from base.exceptions import ObjectNotExist from base.orm import local_session from base.resolvers import query -from orm import ViewedEntry +from orm import ViewedEntry, TopicFollower from orm.reaction import Reaction, ReactionKind -from orm.shout import Shout, ShoutAuthor +from orm.shout import Shout, ShoutAuthor, ShoutTopic +from orm.user import AuthorFollower def add_stat_columns(q): @@ -193,3 +195,43 @@ async def load_shouts_by(_, info, options): shouts_map[shout_id].stat['viewed'] = viewed_stat return shouts + + +@query.field("myFeed") +@login_required +async def get_my_feed(_, info): + auth: AuthCredentials = info.context["request"].auth + user_id = auth.user_id + with local_session() as session: + q = select(Shout).options( + joinedload(Shout.authors), + joinedload(Shout.topics), + ).where( + Shout.deletedAt.is_(None) + ) + + q = q.join( + ShoutAuthor + ).join( + AuthorFollower + ).where( + AuthorFollower.follower == user_id + ).join( + ShoutTopic + ).join( + TopicFollower + ).where(TopicFollower.follower == user_id) + + q = add_stat_columns(q) + + shouts = [] + for [shout, reacted_stat, commented_stat, rating_stat] in session.execute(q).unique(): + shouts.append(shout) + shout.stat = { + "viewed": 0, + "reacted": reacted_stat, + "commented": commented_stat, + "rating": rating_stat + } + + return shouts From e94da1f536ee47da149084f1197c2e63d7494e41 Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Mon, 13 Feb 2023 18:03:09 +0300 Subject: [PATCH 3/9] minorfixes --- orm/reaction.py | 4 ++-- resolvers/create/editor.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/orm/reaction.py b/orm/reaction.py index 3ff769cd..1c129e23 100644 --- a/orm/reaction.py +++ b/orm/reaction.py @@ -19,8 +19,8 @@ class ReactionKind(Enumeration): REJECT = 0 # -1 LIKE = 11 # +1 DISLIKE = 12 # -1 - REMARK = 13 # 0 - FOOTNOTE = 14 # 0 + REMARK = 13 # 0 + FOOTNOTE = 14 # 0 # TYPE = # rating diff diff --git a/resolvers/create/editor.py b/resolvers/create/editor.py index 583a5caf..98283b30 100644 --- a/resolvers/create/editor.py +++ b/resolvers/create/editor.py @@ -12,9 +12,9 @@ from orm.topic import TopicFollower, Topic from orm.user import User from resolvers.zine.reactions import reactions_follow, reactions_unfollow from services.zine.gittask import GitTask -from resolvers.inbox.chats import create_chat -from services.inbox.storage import MessagesStorage -from orm.draft import DraftCollab +# from resolvers.inbox.chats import create_chat +# from services.inbox.storage import MessagesStorage +# from orm.draft import DraftCollab @mutation.field("createShout") From 64e1bea4cdaf4770c239bcb9178d78fce8c36b95 Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Thu, 16 Feb 2023 13:08:55 +0300 Subject: [PATCH 4/9] my feed fixes --- resolvers/zine/following.py | 41 ++++++++++++------------- resolvers/zine/load.py | 61 ++++++++++++++++++++++--------------- schema.graphql | 1 + 3 files changed, 57 insertions(+), 46 deletions(-) diff --git a/resolvers/zine/following.py b/resolvers/zine/following.py index 72c3a0e9..ba3f64b0 100644 --- a/resolvers/zine/following.py +++ b/resolvers/zine/following.py @@ -1,12 +1,12 @@ import asyncio from base.orm import local_session -from base.resolvers import mutation, subscription, query +from base.resolvers import mutation, subscription from auth.authenticate import login_required from auth.credentials import AuthCredentials # from resolvers.community import community_follow, community_unfollow from orm.user import AuthorFollower from orm.topic import TopicFollower -from orm.shout import Shout, ShoutReactionsFollower +from orm.shout import ShoutReactionsFollower from resolvers.zine.profile import author_follow, author_unfollow from resolvers.zine.reactions import reactions_follow, reactions_unfollow from resolvers.zine.topics import topic_follow, topic_unfollow @@ -14,22 +14,6 @@ from services.following import Following, FollowingManager, FollowingResult from graphql.type import GraphQLResolveInfo -@query.field("myFeed") -@login_required -async def get_my_feed(_, info): - auth: AuthCredentials = info.context["request"].auth - user_id = auth.user_id - try: - with local_session() as session: - following_authors = session.query(AuthorFollower).where(AuthorFollower.follower == user_id).unique().all() - following_topics = session.query(TopicFollower).where(TopicFollower.follower == user_id).unique().all() - # TODO: my feed query - shouts = [] - return shouts - except Exception: - pass - - @mutation.field("follow") @login_required async def follow(_, info, what, slug): @@ -99,11 +83,9 @@ async def shout_generator(_, info: GraphQLResolveInfo): tasks = [] with local_session() as session: - following_authors = session.query(AuthorFollower).where( - AuthorFollower.follower == user_id).all() - following_topics = session.query(TopicFollower).where(TopicFollower.follower == user_id).all() - # notify new shout + # notify new shout by followed authors + following_topics = session.query(TopicFollower).where(TopicFollower.follower == user_id).all() for topic_id in following_topics: following_topic = Following('topic', topic_id) @@ -111,12 +93,27 @@ async def shout_generator(_, info: GraphQLResolveInfo): following_topic_task = following_topic.queue.get() tasks.append(following_topic_task) + # by followed topics + following_authors = session.query(AuthorFollower).where( + AuthorFollower.follower == user_id).all() + for author_id in following_authors: following_author = Following('author', author_id) await FollowingManager.register('author', following_author) following_author_task = following_author.queue.get() tasks.append(following_author_task) + # TODO: use communities + # by followed communities + # following_communities = session.query(CommunityFollower).where( + # CommunityFollower.follower == user_id).all() + + # for community_id in following_communities: + # following_community = Following('community', author_id) + # await FollowingManager.register('community', following_community) + # following_community_task = following_community.queue.get() + # tasks.append(following_community_task) + while True: shout = await asyncio.gather(*tasks) yield shout diff --git a/resolvers/zine/load.py b/resolvers/zine/load.py index 0dfc9468..6c42fad0 100644 --- a/resolvers/zine/load.py +++ b/resolvers/zine/load.py @@ -199,32 +199,45 @@ async def load_shouts_by(_, info, options): @query.field("myFeed") @login_required -async def get_my_feed(_, info): +async def get_my_feed(_, info, options): auth: AuthCredentials = info.context["request"].auth user_id = auth.user_id + + q = select(Shout).options( + joinedload(Shout.authors), + joinedload(Shout.topics), + ).where( + Shout.deletedAt.is_(None) + ) + + q = q.join( + ShoutAuthor + ).join( + AuthorFollower + ).where( + AuthorFollower.follower == user_id + ).join( + ShoutTopic + ).join( + TopicFollower + ).where(TopicFollower.follower == user_id) + + q = add_stat_columns(q) + q = apply_filters(q, options.get("filters", {}), 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')) + + 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) + + q = q.group_by(Shout.id).order_by(query_order_by).limit(limit).offset(offset) + + shouts = [] with local_session() as session: - q = select(Shout).options( - joinedload(Shout.authors), - joinedload(Shout.topics), - ).where( - Shout.deletedAt.is_(None) - ) - - q = q.join( - ShoutAuthor - ).join( - AuthorFollower - ).where( - AuthorFollower.follower == user_id - ).join( - ShoutTopic - ).join( - TopicFollower - ).where(TopicFollower.follower == user_id) - - q = add_stat_columns(q) - - shouts = [] for [shout, reacted_stat, commented_stat, rating_stat] in session.execute(q).unique(): shouts.append(shout) shout.stat = { @@ -234,4 +247,4 @@ async def get_my_feed(_, info): "rating": rating_stat } - return shouts + return shouts diff --git a/schema.graphql b/schema.graphql index f780d0fb..e4f99856 100644 --- a/schema.graphql +++ b/schema.graphql @@ -302,6 +302,7 @@ type Query { userFollowedTopics(slug: String!): [Topic]! authorsAll: [Author]! getAuthor(slug: String!): User + myFeed(options: LoadShoutsOptions): [Shout] # draft/collab loadDrafts: [DraftCollab]! From 2b91f5a529c09cb91d076ea7038acfa12d8c9b44 Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Fri, 17 Feb 2023 17:30:38 +0300 Subject: [PATCH 5/9] draft2shout --- orm/draft.py | 1 + orm/shout.py | 40 ++++---- resolvers/auth.py | 3 +- resolvers/create/{collab.py => drafts.py} | 109 ++++++++++++++++------ 4 files changed, 109 insertions(+), 44 deletions(-) rename resolvers/create/{collab.py => drafts.py} (51%) diff --git a/orm/draft.py b/orm/draft.py index 6b97e5c3..bae7f1c0 100644 --- a/orm/draft.py +++ b/orm/draft.py @@ -13,6 +13,7 @@ class DraftTopic(Base): id = None # type: ignore collab = Column(ForeignKey("draft_collab.id"), primary_key=True) topic = Column(ForeignKey("topic.id"), primary_key=True) + main = Column(Boolean, default=False) class DraftAuthor(Base): diff --git a/orm/shout.py b/orm/shout.py index 04b0102b..acfdbe81 100644 --- a/orm/shout.py +++ b/orm/shout.py @@ -15,6 +15,7 @@ class ShoutTopic(Base): id = None # type: ignore shout = Column(ForeignKey("shout.id"), primary_key=True, index=True) topic = Column(ForeignKey("topic.id"), primary_key=True, index=True) + main = Column(Boolean, default=False) class ShoutReactionsFollower(Base): @@ -42,28 +43,33 @@ class ShoutAuthor(Base): class Shout(Base): __tablename__ = "shout" - slug = Column(String, unique=True) - community = Column(ForeignKey("community.id"), default=1) - lang = Column(String, nullable=False, default='ru', comment="Language") - body = Column(String, nullable=False, comment="Body") - title = Column(String, nullable=True) - subtitle = Column(String, nullable=True) - layout = Column(String, nullable=True) - mainTopic = Column(ForeignKey("topic.slug"), nullable=True) - cover = Column(String, nullable=True, comment="Cover") - authors = relationship(lambda: User, secondary=ShoutAuthor.__tablename__) - topics = relationship(lambda: Topic, secondary=ShoutTopic.__tablename__) - reactions = relationship(lambda: Reaction) - visibility = Column(String, nullable=True) # owner authors community public - versionOf = Column(ForeignKey("shout.id"), nullable=True) - oid = Column(String, nullable=True) - media = Column(JSON, nullable=True) - + # timestamps createdAt = Column(DateTime, nullable=False, default=datetime.now, comment="Created at") updatedAt = Column(DateTime, nullable=True, comment="Updated at") publishedAt = Column(DateTime, nullable=True) deletedAt = Column(DateTime, nullable=True) + # same with Draft + slug = Column(String, unique=True) + cover = Column(String, nullable=True, comment="Cover") + body = Column(String, nullable=False, comment="Body") + title = Column(String, nullable=True) + subtitle = Column(String, nullable=True) + layout = Column(String, nullable=True) + media = Column(JSON, nullable=True) + authors = relationship(lambda: User, secondary=ShoutAuthor.__tablename__) + topics = relationship(lambda: Topic, secondary=ShoutTopic.__tablename__) + + reactions = relationship(lambda: Reaction) + + # TODO: these field should be used or modified + community = Column(ForeignKey("community.id"), default=1) + lang = Column(String, nullable=False, default='ru', comment="Language") + mainTopic = Column(ForeignKey("topic.slug"), nullable=True) + visibility = Column(String, nullable=True) # owner authors community public + versionOf = Column(ForeignKey("shout.id"), nullable=True) + oid = Column(String, nullable=True) + @staticmethod def init_table(): with local_session() as session: diff --git a/resolvers/auth.py b/resolvers/auth.py index 6108a827..ea42f409 100644 --- a/resolvers/auth.py +++ b/resolvers/auth.py @@ -6,7 +6,7 @@ from urllib.parse import quote_plus from graphql.type import GraphQLResolveInfo from starlette.responses import RedirectResponse from transliterate import translit - +import re from auth.authenticate import login_required from auth.credentials import AuthCredentials from auth.email import send_auth_email @@ -92,6 +92,7 @@ def create_user(user_dict): def generate_unique_slug(src): print('[resolvers.auth] generating slug from: ' + src) slug = translit(src, "ru", reversed=True).replace(".", "-").lower() + slug = re.sub('[^0-9a-zA-Z]+', '-', slug) if slug != src: print('[resolvers.auth] translited name: ' + slug) c = 1 diff --git a/resolvers/create/collab.py b/resolvers/create/drafts.py similarity index 51% rename from resolvers/create/collab.py rename to resolvers/create/drafts.py index f8b1c8a7..de33481e 100644 --- a/resolvers/create/collab.py +++ b/resolvers/create/drafts.py @@ -2,10 +2,13 @@ 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 -from orm.draft import DraftCollab, DraftAuthor, DraftTopic +from orm.draft import DraftCollab, DraftAuthor from orm.shout import Shout +from orm.topic import Topic from orm.user import User +from datetime import datetime, timezone +from transliterate import translit +import re @query.field("loadDrafts") @@ -18,11 +21,11 @@ async def load_drafts(_, info): return drafts -@mutation.field("createDraft") # TODO +@mutation.field("createDraft") # TODO @login_required async def create_draft(_, info, draft_input): auth: AuthCredentials = info.context["request"].auth - + draft_input['createdBy'] = auth.user_id with local_session() as session: collab = DraftCollab.create(**draft_input) session.add(collab) @@ -32,52 +35,56 @@ async def create_draft(_, info, draft_input): return {} -@mutation.field("deleteDraft") # TODO +@mutation.field("deleteDraft") @login_required async def delete_draft(_, info, draft: int = 0): auth: AuthCredentials = info.context["request"].auth - with local_session() as session: - collab = session.query(DraftCollab).where(DraftCollab.id == draft_input.id).one() - if auth.user_id not in s.authors: + d = session.query(DraftCollab).where(DraftCollab.id == draft).one() + if auth.user_id not in d.authors: # raise BaseHttpException("only owner can remove coauthors") return { "error": "Only authors can update a draft" } - elif not collab: + elif not d: return { "error": "There is no draft with this id" } else: - session.delete(collab) + session.delete(d) session.commit() return {} -@mutation.field("updateDraft") # TODO: draft input type +@mutation.field("updateDraft") # TODO: draft input type @login_required async def update_draft(_, info, draft_input): auth: AuthCredentials = info.context["request"].auth with local_session() as session: - collab = session.query(DraftCollab).where(DraftCollab.id == draft_input.id).one() # raises Error when not found - if auth.user_id not in s.authors: + d = session.query( + DraftCollab + ).where( + DraftCollab.id == draft_input.id + ).one() # raises Error when not found + if auth.user_id not in d.authors: # raise BaseHttpException("only owner can remove coauthors") return { "error": "Only authors can update draft" } - elif not s: + elif not d: return { "error": "There is no draft with this id" } else: draft_input["updatedAt"] = datetime.now(tz=timezone.utc) - collab.update(draft_input) + d.update(draft_input) session.commit() # TODO: email notify return {} + @mutation.field("inviteAuthor") @login_required async def invite_coauthor(_, info, author: int = 0, draft: int = 0): @@ -108,25 +115,75 @@ async def invite_coauthor(_, info, author: int = 0, draft: int = 0): return {} +def get_slug(src): + slug = translit(src, "ru", reversed=True).replace(".", "-").lower() + slug = re.sub('[^0-9a-zA-Z]+', '-', slug) + return slug + + @mutation.field("inviteAccept") @login_required async def accept_coauthor(_, info, draft: int): auth: AuthCredentials = info.context["request"].auth with local_session() as session: - # c = session.query(DraftCollab).where(DraftCollab.id == draft).one() - a = session.query(DraftAuthor).where(DraftAuthor.collab == draft).filter(DraftAuthor.author == auth.user_id).one() - if not a.accepted: - a.accepted = True - session.commit() - # TODO: email notify - return {} - elif a.accepted == True: + d = session.query(DraftCollab).where(DraftCollab.id == draft).one() + if not d: return { - "error": "You have accepted invite before" + "error": "Draft id was not found" } else: - # raise BaseHttpException("only invited can accept") + a = session.query(DraftAuthor).where(DraftAuthor.collab == draft).filter( + DraftAuthor.author == auth.user_id).one() + if not a.accepted: + a.accepted = True + session.commit() + # TODO: email notify + return {} + elif a.accepted: + return { + "error": "You have accepted invite before" + } + else: + # raise BaseHttpException("only invited can accept") + return { + "error": "You don't have an invitation yet" + } + + +@mutation.field("draftToShout") +@login_required +async def draft_to_shout(_, info, draft: int = 0): + auth: AuthCredentials = info.context["request"].auth + + with local_session() as session: + d = session.query(DraftCollab).where(DraftCollab.id == draft).one() + if auth.user_id not in d.authors: + # raise BaseHttpException("you are not in authors list") return { - "error": "You don't have an invitation yet" + "error": "You are not in authors list" } + elif d.id: + draft_authors = [a.author for a in d.authors] + draft_topics = [t.topic for t in d.topics] + authors = session.query(User).where(User.id._in(draft_authors)).all() + topics = session.query(Topic).where(Topic.id._in(draft_topics)).all() + new_shout = Shout.create({ + "authors": authors, + "body": d.body, + "title": d.title, + "subtitle": d.subtitle or "", + "topics": topics, + "media": d.media, + "slug": d.slug or get_slug(d.title), + "layout": d.layout or "article" + }) + session.add(new_shout) + session.commit() + else: + return { + "error": "Draft is not found" + } + + # TODO: email notify + return {} From 9fb6c72dbe5992b024f604790dd1f237150bead5 Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Mon, 20 Feb 2023 18:32:32 +0300 Subject: [PATCH 6/9] drafts-fixes --- resolvers/__init__.py | 9 ++++----- schema.graphql | 4 +++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/resolvers/__init__.py b/resolvers/__init__.py index 5837f767..df652076 100644 --- a/resolvers/__init__.py +++ b/resolvers/__init__.py @@ -8,8 +8,8 @@ from resolvers.auth import ( get_current_user, ) -from resolvers.create.collab import load_drafts, create_draft, update_draft, delete_draft,\ - accept_coauthor, invite_coauthor +from resolvers.create.drafts import load_drafts, create_draft, update_draft, delete_draft,\ + accept_coauthor, invite_coauthor, draft_to_shout from resolvers.create.migrate import markdown_body from resolvers.create.editor import create_shout, delete_shout, update_shout @@ -87,19 +87,18 @@ __all__ = [ # zine.following "follow", "unfollow", - # create.editor + # create "create_shout", "update_shout", "delete_shout", - # create.migrate "markdown_body", - # create.collab "load_drafts", "create_draft", "update_draft", "delete_draft", "invite_coauthor", "accept_coauthor", + "draft_to_shout", # zine.topics "topics_all", "topics_by_community", diff --git a/schema.graphql b/schema.graphql index e4f99856..3598ba57 100644 --- a/schema.graphql +++ b/schema.graphql @@ -69,7 +69,6 @@ type Result { members: [ChatMember] shout: Shout shouts: [Shout] - drafts: [DraftCollab] author: Author authors: [Author] reaction: Reaction @@ -78,6 +77,8 @@ type Result { topics: [Topic] community: Community communities: [Community] + draft: DraftCollab + drafts: [DraftCollab] } enum ReactionStatus { @@ -207,6 +208,7 @@ type Mutation { deleteDraft(draft: Int!): Result! inviteAccept(draft: Int!): Result! inviteAuthor(draft: Int!, author: Int!): Result! + draftToShout(draft: Int!): Result! # following follow(what: FollowingEntity!, slug: String!): Result! From a8ad52cabadf2ef22c902a94c87a6542582b1ac2 Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Mon, 20 Feb 2023 19:09:55 +0300 Subject: [PATCH 7/9] auth fix --- auth/authenticate.py | 62 +++++++++++++++++++------------------------- auth/jwtcodec.py | 1 + 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/auth/authenticate.py b/auth/authenticate.py index d01cefc3..70ad5018 100644 --- a/auth/authenticate.py +++ b/auth/authenticate.py @@ -12,7 +12,7 @@ from orm.user import User, Role from settings import SESSION_TOKEN_HEADER from auth.tokenstorage import SessionToken -from base.exceptions import InvalidToken, OperationNotAllowed +from base.exceptions import OperationNotAllowed class JWTAuthenticate(AuthenticationBackend): @@ -30,44 +30,34 @@ class JWTAuthenticate(AuthenticationBackend): user_id=None ) - try: - if len(token.split('.')) > 1: - payload = await SessionToken.verify(token) - if payload is None: - return AuthCredentials(scopes=[]), AuthUser(user_id=None) - user = None - with local_session() as session: - try: - 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 + if len(token.split('.')) > 1: + payload = await SessionToken.verify(token) + user = None + with local_session() as session: + try: + user = ( + session.query(User).options( + joinedload(User.roles).options(joinedload(Role.permissions)), + joinedload(User.ratings) + ).filter( + User.id == payload.user_id + ).one() + ) - if not user: - return AuthCredentials(scopes=[]), AuthUser(user_id=None) + scopes = {} # TODO: integrate await user.get_permission() - scopes = {} # await user.get_permission() + return ( + AuthCredentials( + user_id=payload.user_id, + scopes=scopes, + logged_in=True + ), + AuthUser(user_id=user.id), + ) + except exc.NoResultFound: + pass - return ( - AuthCredentials( - user_id=payload.user_id, - scopes=scopes, - logged_in=True - ), - AuthUser(user_id=user.id), - ) - else: - InvalidToken("please try again") - except Exception as e: - print("[auth.authenticate] session token verify error") - print(e) - return AuthCredentials(scopes=[], error_message=str(e)), AuthUser(user_id=None) + return AuthCredentials(scopes=[], error_message=str('Invalid token')), AuthUser(user_id=None) def login_required(func): diff --git a/auth/jwtcodec.py b/auth/jwtcodec.py index 7f176e58..d4d2116f 100644 --- a/auth/jwtcodec.py +++ b/auth/jwtcodec.py @@ -23,6 +23,7 @@ class JWTCodec: @staticmethod def decode(token: str, verify_exp: bool = True) -> TokenPayload: r = None + payload = None try: payload = jwt.decode( token, From 80030f21b79ae6f8d4b2ce737c45aecc933743a9 Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Mon, 20 Feb 2023 20:38:20 +0300 Subject: [PATCH 8/9] following manager works --- resolvers/inbox/messages.py | 6 ++-- resolvers/zine/following.py | 51 +++++++++++++++--------------- resolvers/zine/profile.py | 23 +++++++------- resolvers/zine/reactions.py | 63 +++++++++++++++++++++---------------- resolvers/zine/topics.py | 45 +++++++++++++++----------- services/following.py | 21 +++++++------ 6 files changed, 114 insertions(+), 95 deletions(-) diff --git a/resolvers/inbox/messages.py b/resolvers/inbox/messages.py index d33af9c3..44ff1f03 100644 --- a/resolvers/inbox/messages.py +++ b/resolvers/inbox/messages.py @@ -51,7 +51,7 @@ async def create_message(_, info, chat: str, body: str, replyTo=None): ) result = FollowingResult("NEW", 'chat', new_message) - await FollowingManager.put('chat', result) + await FollowingManager.push('chat', result) return { "message": new_message, @@ -82,7 +82,7 @@ async def update_message(_, info, chat_id: str, message_id: int, body: str): await redis.execute("SET", f"chats/{chat_id}/messages/{message_id}", json.dumps(message)) result = FollowingResult("UPDATED", 'chat', message) - await FollowingManager.put('chat', result) + await FollowingManager.push('chat', result) return { "message": message, @@ -115,7 +115,7 @@ async def delete_message(_, info, chat_id: str, message_id: int): await redis.execute("LREM", f"chats/{chat_id}/unread/{user_id}", 0, str(message_id)) result = FollowingResult("DELETED", 'chat', message) - await FollowingManager.put(result) + await FollowingManager.push(result) return {} diff --git a/resolvers/zine/following.py b/resolvers/zine/following.py index ba3f64b0..b2e039f1 100644 --- a/resolvers/zine/following.py +++ b/resolvers/zine/following.py @@ -21,23 +21,23 @@ async def follow(_, info, what, slug): try: if what == "AUTHOR": - author_follow(auth.user_id, slug) - result = FollowingResult("NEW", 'author', slug) - await FollowingManager.put('author', result) + if author_follow(auth.user_id, slug): + result = FollowingResult("NEW", 'author', slug) + await FollowingManager.push('author', result) elif what == "TOPIC": - topic_follow(auth.user_id, slug) - result = FollowingResult("NEW", 'topic', slug) - await FollowingManager.put('topic', result) + if topic_follow(auth.user_id, slug): + result = FollowingResult("NEW", 'topic', slug) + await FollowingManager.push('topic', result) elif what == "COMMUNITY": - # community_follow(user, slug) - # result = FollowingResult("NEW", 'community', slug) - # await FollowingManager.put('community', result) - pass + if False: # TODO: use community_follow(auth.user_id, slug): + result = FollowingResult("NEW", 'community', slug) + await FollowingManager.push('community', result) elif what == "REACTIONS": - reactions_follow(auth.user_id, slug) - result = FollowingResult("NEW", 'shout', slug) - await FollowingManager.put('shout', result) + if reactions_follow(auth.user_id, slug): + result = FollowingResult("NEW", 'shout', slug) + await FollowingManager.push('shout', result) except Exception as e: + print(Exception(e)) return {"error": str(e)} return {} @@ -50,22 +50,21 @@ async def unfollow(_, info, what, slug): try: if what == "AUTHOR": - author_unfollow(auth.user_id, slug) - result = FollowingResult("DELETED", 'author', slug) - await FollowingManager.put('author', result) + if author_unfollow(auth.user_id, slug): + result = FollowingResult("DELETED", 'author', slug) + await FollowingManager.push('author', result) elif what == "TOPIC": - topic_unfollow(auth.user_id, slug) - result = FollowingResult("DELETED", 'topic', slug) - await FollowingManager.put('topic', result) + if topic_unfollow(auth.user_id, slug): + result = FollowingResult("DELETED", 'topic', slug) + await FollowingManager.push('topic', result) elif what == "COMMUNITY": - # community_unfollow(user, slug) - # result = FollowingResult("DELETED", 'community', slug) - # await FollowingManager.put('community', result) - pass + if False: # TODO: use community_unfollow(auth.user_id, slug): + result = FollowingResult("DELETED", 'community', slug) + await FollowingManager.push('community', result) elif what == "REACTIONS": - reactions_unfollow(auth.user_id, slug) - result = FollowingResult("DELETED", 'shout', slug) - await FollowingManager.put('shout', result) + if reactions_unfollow(auth.user_id, slug): + result = FollowingResult("DELETED", 'shout', slug) + await FollowingManager.push('shout', result) except Exception as e: return {"error": str(e)} diff --git a/resolvers/zine/profile.py b/resolvers/zine/profile.py index a875fbf9..ddcafc33 100644 --- a/resolvers/zine/profile.py +++ b/resolvers/zine/profile.py @@ -198,11 +198,15 @@ async def rate_user(_, info, rated_userslug, value): # for mutation.field("follow") 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) - session.add(af) - session.commit() + try: + with local_session() as session: + author = session.query(User).where(User.slug == slug).one() + af = AuthorFollower.create(follower=user_id, author=author.id) + session.add(af) + session.commit() + return True + except: + return False # for mutation.field("unfollow") @@ -217,14 +221,11 @@ def author_unfollow(user_id, slug): ) ).first() ) - if not flw: - return { - "error": "Follower is not exist, cant unfollow" - } - else: + if flw: session.delete(flw) session.commit() - return {} + return True + return False @query.field("authorsAll") diff --git a/resolvers/zine/reactions.py b/resolvers/zine/reactions.py index 7200e8bc..05a57a0d 100644 --- a/resolvers/zine/reactions.py +++ b/resolvers/zine/reactions.py @@ -40,40 +40,49 @@ def add_reaction_stat_columns(q): def reactions_follow(user_id, shout_id: int, auto=False): - with local_session() as session: - shout = session.query(Shout).where(Shout.id == shout_id).one() + try: + with local_session() as session: + shout = session.query(Shout).where(Shout.id == shout_id).one() - following = ( - session.query(ShoutReactionsFollower).where(and_( - ShoutReactionsFollower.follower == user_id, - ShoutReactionsFollower.shout == shout.id, - )).first() - ) - - if not following: - following = ShoutReactionsFollower.create( - follower=user_id, - shout=shout.id, - auto=auto + following = ( + session.query(ShoutReactionsFollower).where(and_( + ShoutReactionsFollower.follower == user_id, + ShoutReactionsFollower.shout == shout.id, + )).first() ) - session.add(following) - session.commit() + + if not following: + following = ShoutReactionsFollower.create( + follower=user_id, + shout=shout.id, + auto=auto + ) + session.add(following) + session.commit() + return True + except: + return False def reactions_unfollow(user_id: int, shout_id: int): - with local_session() as session: - shout = session.query(Shout).where(Shout.id == shout_id).one() + try: + with local_session() as session: + shout = session.query(Shout).where(Shout.id == shout_id).one() - following = ( - session.query(ShoutReactionsFollower).where(and_( - ShoutReactionsFollower.follower == user_id, - ShoutReactionsFollower.shout == shout.id - )).first() - ) + following = ( + session.query(ShoutReactionsFollower).where(and_( + ShoutReactionsFollower.follower == user_id, + ShoutReactionsFollower.shout == shout.id + )).first() + ) - if following: - session.delete(following) - session.commit() + if following: + session.delete(following) + session.commit() + return True + except: + pass + return False def is_published_author(session, user_id): diff --git a/resolvers/zine/topics.py b/resolvers/zine/topics.py index 1eebdd60..f354a7b4 100644 --- a/resolvers/zine/topics.py +++ b/resolvers/zine/topics.py @@ -117,29 +117,36 @@ async def update_topic(_, _info, inp): def topic_follow(user_id, slug): - with local_session() as session: - topic = session.query(Topic).where(Topic.slug == slug).one() + try: + with local_session() as session: + topic = session.query(Topic).where(Topic.slug == slug).one() - following = TopicFollower.create(topic=topic.id, follower=user_id) - session.add(following) - session.commit() + following = TopicFollower.create(topic=topic.id, follower=user_id) + session.add(following) + session.commit() + return True + except: + return False def topic_unfollow(user_id, slug): - with local_session() as session: - sub = ( - session.query(TopicFollower).join(Topic).filter( - and_( - TopicFollower.follower == user_id, - Topic.slug == slug - ) - ).first() - ) - if not sub: - raise Exception("[resolvers.topics] follower not exist") - else: - session.delete(sub) - session.commit() + try: + with local_session() as session: + sub = ( + session.query(TopicFollower).join(Topic).filter( + and_( + TopicFollower.follower == user_id, + Topic.slug == slug + ) + ).first() + ) + if sub: + session.delete(sub) + session.commit() + return True + except: + pass + return False @query.field("topicsRandom") diff --git a/services/following.py b/services/following.py index f67f78a2..8410eb2d 100644 --- a/services/following.py +++ b/services/following.py @@ -37,12 +37,15 @@ class FollowingManager: @staticmethod async def push(kind, payload): - async with FollowingManager.lock: - if kind == 'chat': - for chat in FollowingManager['chat']: - if payload.message["chatId"] == chat.uid: - chat.queue.put_nowait(payload) - else: - for entity in FollowingManager[kind]: - if payload.shout['createdBy'] == entity.uid: - entity.queue.put_nowait(payload) + try: + async with FollowingManager.lock: + if kind == 'chat': + for chat in FollowingManager['chat']: + if payload.message["chatId"] == chat.uid: + chat.queue.put_nowait(payload) + else: + for entity in FollowingManager[kind]: + if payload.shout['createdBy'] == entity.uid: + entity.queue.put_nowait(payload) + except Exception as e: + print(Exception(e)) From 53986576f51e73bd555c0eab2677ae05c61a3074 Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Mon, 20 Feb 2023 20:38:59 +0300 Subject: [PATCH 9/9] load recipients fixes --- resolvers/inbox/load.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/resolvers/inbox/load.py b/resolvers/inbox/load.py index b5909d8c..a0d41721 100644 --- a/resolvers/inbox/load.py +++ b/resolvers/inbox/load.py @@ -50,12 +50,14 @@ async def load_chats(_, info, limit: int = 50, offset: int = 0): auth: AuthCredentials = info.context["request"].auth cids = await redis.execute("SMEMBERS", "chats_by_user/" + str(auth.user_id)) - onliners = await redis.execute("SMEMBERS", "users-online") if cids: cids = list(cids)[offset:offset + limit] if not cids: print('[inbox.load] no chats were found') cids = [] + onliners = await redis.execute("SMEMBERS", "users-online") + if not onliners: + onliners = [] chats = [] for cid in cids: cid = cid.decode("utf-8") @@ -124,8 +126,10 @@ async def load_messages_by(_, info, by, limit: int = 10, offset: int = 0): async def load_recipients(_, info, limit=50, offset=0): chat_users = [] auth: AuthCredentials = info.context["request"].auth + onliners = await redis.execute("SMEMBERS", "users-online") + if not onliners: + onliners = [] try: - onliners = await redis.execute("SMEMBERS", "users-online") chat_users += await followed_authors(auth.user_id) limit = limit - len(chat_users) except Exception: