From 9942fc255844e83635d6a8e3b44f7db13e520cdc Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Tue, 15 Nov 2022 05:36:30 +0300 Subject: [PATCH] load-by pattern, shoutscache removed --- create_crt.sh | 14 -- main.py | 3 - orm/shout.py | 1 - resolvers/__init__.py | 105 +++++-------- resolvers/collection.py | 104 ------------ resolvers/community.py | 134 ---------------- resolvers/editor.py | 11 +- resolvers/feed.py | 53 ------- resolvers/inbox/chats.py | 37 +---- resolvers/inbox/load.py | 79 +++++++--- resolvers/inbox/messages.py | 19 +-- resolvers/inbox/search.py | 36 ----- resolvers/profile.py | 86 +++++----- resolvers/reactions.py | 110 +++++++------ resolvers/topics.py | 7 +- resolvers/zine.py | 297 ++++++++--------------------------- schema.graphql | 126 ++++++--------- services/stat/reacted.py | 10 ++ services/zine/shoutscache.py | 285 --------------------------------- 19 files changed, 325 insertions(+), 1192 deletions(-) delete mode 100644 create_crt.sh delete mode 100644 resolvers/collection.py delete mode 100644 resolvers/community.py delete mode 100644 resolvers/feed.py delete mode 100644 services/zine/shoutscache.py diff --git a/create_crt.sh b/create_crt.sh deleted file mode 100644 index 9994eb82..00000000 --- a/create_crt.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -openssl req -newkey rsa:4096 \ - -x509 \ - -sha256 \ - -days 3650 \ - -nodes \ - -out server.crt \ - -keyout server.key \ - -subj "/C=RU/ST=Moscow/L=Moscow/O=Discours/OU=Site/CN=newapi.discours.io" - -openssl x509 -in server.crt -out server.pem -outform PEM -tar cvf server.tar server.crt server.key -dokku certs:add discoursio-api < server.tar diff --git a/main.py b/main.py index 10bd865e..a4d090f0 100644 --- a/main.py +++ b/main.py @@ -14,7 +14,6 @@ from auth.oauth import oauth_login, oauth_authorize from base.redis import redis from base.resolvers import resolvers from resolvers.auth import confirm_email_handler -from resolvers.zine import ShoutsCache from services.main import storages_init from services.stat.reacted import ReactedStorage from services.stat.topicstat import TopicStat @@ -36,8 +35,6 @@ async def start_up(): print(viewed_storage_task) reacted_storage_task = asyncio.create_task(ReactedStorage.worker()) print(reacted_storage_task) - shouts_cache_task = asyncio.create_task(ShoutsCache.worker()) - print(shouts_cache_task) shout_author_task = asyncio.create_task(ShoutAuthorStorage.worker()) print(shout_author_task) topic_stat_task = asyncio.create_task(TopicStat.worker()) diff --git a/orm/shout.py b/orm/shout.py index ec954711..2a3909ad 100644 --- a/orm/shout.py +++ b/orm/shout.py @@ -64,4 +64,3 @@ class Shout(Base): updatedAt = Column(DateTime, nullable=True, comment="Updated at") publishedAt = Column(DateTime, nullable=True) deletedAt = Column(DateTime, nullable=True) - diff --git a/resolvers/__init__.py b/resolvers/__init__.py index 1fe2933f..25b516b2 100644 --- a/resolvers/__init__.py +++ b/resolvers/__init__.py @@ -8,32 +8,23 @@ from resolvers.auth import ( get_current_user, ) from resolvers.collab import remove_author, invite_author -from resolvers.community import ( - create_community, - delete_community, - get_community, - get_communities, -) - from resolvers.migrate import markdown_body # from resolvers.collab import invite_author, remove_author from resolvers.editor import create_shout, delete_shout, update_shout from resolvers.profile import ( - get_users_by_slugs, - get_user_reacted_shouts, - get_user_roles, - get_top_authors, - get_author + load_authors_by, + rate_user, + update_profile ) -# from resolvers.feed import shouts_for_feed, my_candidates from resolvers.reactions import ( create_reaction, delete_reaction, update_reaction, reactions_unfollow, reactions_follow, + load_reactions_by ) from resolvers.topics import ( topic_follow, @@ -45,36 +36,31 @@ from resolvers.topics import ( ) from resolvers.zine import ( - get_shout_by_slug, follow, unfollow, - increment_view, - top_month, - top_overall, - recent_published, - recent_all, - recent_commented, - recent_reacted, - shouts_by_authors, - shouts_by_topics, - shouts_by_layout_recent, - shouts_by_layout_top, - shouts_by_layout_topmonth, - shouts_by_communities, + load_shouts_by ) -from resolvers.inbox.chats import load_chats, \ - create_chat, delete_chat, update_chat, \ - invite_to_chat, enter_chat -from resolvers.inbox.messages import load_chat_messages, \ - create_message, delete_message, update_message, \ - message_generator, mark_as_read -from resolvers.inbox.search import search_users, \ - search_messages, search_chats +from resolvers.inbox.chats import ( + create_chat, + delete_chat, + update_chat, + invite_to_chat +) +from resolvers.inbox.messages import ( + create_message, + delete_message, + update_message, + message_generator, + mark_as_read +) +from resolvers.inbox.load import ( + load_chats, + load_messages_by +) +from resolvers.inbox.search import search_users __all__ = [ - "follow", - "unfollow", # auth "login", "register_by_email", @@ -83,27 +69,15 @@ __all__ = [ "auth_send_link", "sign_out", "get_current_user", - # profile - "get_users_by_slugs", - "get_user_roles", - "get_top_authors", - "get_author", + # authors + "load_authors_by", + "rate_user", + "update_profile", + "get_authors_all", # zine - "recent_published", - "recent_commented", - "recent_reacted", - "recent_all", - "shouts_by_topics", - "shouts_by_layout_recent", - "shouts_by_layout_topmonth", - "shouts_by_layout_top", - "shouts_by_authors", - "shouts_by_communities", - "get_user_reacted_shouts", - "top_month", - "top_overall", - "increment_view", - "get_shout_by_slug", + "load_shouts_by", + "follow", + "unfollow", # editor "create_shout", "update_shout", @@ -120,31 +94,24 @@ __all__ = [ "topic_follow", "topic_unfollow", "get_topic", - # communities - "get_community", - "get_communities", - "create_community", - "delete_community", # reactions "reactions_follow", "reactions_unfollow", "create_reaction", "update_reaction", "delete_reaction", + "load_reactions_by", # inbox + "load_chats", + "load_messages_by", + "invite_to_chat", "create_chat", "delete_chat", "update_chat", - "load_chats", "create_message", "delete_message", "update_message", - "load_chat_messages", "message_generator", "mark_as_read", - "search_users", - "search_chats", - "search_messages", - "enter_chat", - "invite_to_chat" + "search_users" ] diff --git a/resolvers/collection.py b/resolvers/collection.py deleted file mode 100644 index 9c300b3b..00000000 --- a/resolvers/collection.py +++ /dev/null @@ -1,104 +0,0 @@ -from datetime import datetime - -from sqlalchemy import and_ - -from auth.authenticate import login_required -from base.orm import local_session -from base.resolvers import mutation, query -from orm.collection import Collection, ShoutCollection -from orm.user import User - - -@mutation.field("createCollection") -@login_required -async def create_collection(_, _info, inp): - # auth = info.context["request"].auth - # user_id = auth.user_id - collection = Collection.create( - slug=inp.get("slug", ""), - title=inp.get("title", ""), - desc=inp.get("desc", ""), - pic=inp.get("pic", ""), - ) - - return {"collection": collection} - - -@mutation.field("updateCollection") -@login_required -async def update_collection(_, info, inp): - auth = info.context["request"].auth - user_id = auth.user_id - collection_slug = inp.get("slug", "") - with local_session() as session: - owner = session.query(User).filter(User.id == user_id) # note list here - collection = ( - session.query(Collection).filter(Collection.slug == collection_slug).first() - ) - editors = [e.slug for e in collection.editors] - if not collection: - return {"error": "invalid collection id"} - if collection.createdBy not in (owner + editors): - return {"error": "access denied"} - collection.title = inp.get("title", "") - collection.desc = inp.get("desc", "") - collection.pic = inp.get("pic", "") - collection.updatedAt = datetime.now() - session.commit() - - -@mutation.field("deleteCollection") -@login_required -async def delete_collection(_, info, slug): - auth = info.context["request"].auth - user_id = auth.user_id - with local_session() as session: - collection = session.query(Collection).filter(Collection.slug == slug).first() - if not collection: - return {"error": "invalid collection slug"} - if collection.owner != user_id: - return {"error": "access denied"} - collection.deletedAt = datetime.now() - session.add(collection) - session.commit() - - return {} - - -@query.field("getUserCollections") -async def get_user_collections(_, _info, userslug): - collections = [] - with local_session() as session: - user = session.query(User).filter(User.slug == userslug).first() - if user: - # TODO: check rights here - collections = ( - session.query(Collection) - .where( - and_(Collection.createdBy == userslug, Collection.publishedAt.is_not(None)) - ) - .all() - ) - for c in collections: - shouts = ( - session.query(ShoutCollection) - .filter(ShoutCollection.collection == c.id) - .all() - ) - c.amount = len(shouts) - return collections - - -@query.field("getMyColelctions") -@login_required -async def get_my_collections(_, info): - auth = info.context["request"].auth - user_id = auth.user_id - with local_session() as session: - collections = ( - session.query(Collection).when(Collection.createdBy == user_id).all() - ) - return collections - - -# TODO: get shouts list by collection diff --git a/resolvers/community.py b/resolvers/community.py deleted file mode 100644 index e37e0e7e..00000000 --- a/resolvers/community.py +++ /dev/null @@ -1,134 +0,0 @@ -from datetime import datetime -from typing import List - -from sqlalchemy import and_ - -from auth.authenticate import login_required -from base.orm import local_session -from base.resolvers import mutation, query -from orm.community import Community, CommunityFollower -from orm.user import User - - -@mutation.field("createCommunity") -@login_required -async def create_community(_, info, input): - auth = info.context["request"].auth - user_id = auth.user_id - with local_session() as session: - user = session.query(User).where(User.id == user_id).first() - community = Community.create( - slug=input.get("slug", ""), - title=input.get("title", ""), - desc=input.get("desc", ""), - pic=input.get("pic", ""), - createdBy=user.slug, - createdAt=datetime.now(), - ) - session.add(community) - session.commit() - - return {"community": community} - - -@mutation.field("updateCommunity") -@login_required -async def update_community(_, info, input): - auth = info.context["request"].auth - user_id = auth.user_id - community_slug = input.get("slug", "") - - with local_session() as session: - owner = session.query(User).filter(User.id == user_id) # note list here - community = ( - session.query(Community).filter(Community.slug == community_slug).first() - ) - editors = [e.slug for e in community.editors] - if not community: - return {"error": "invalid community id"} - if community.createdBy not in (owner + editors): - return {"error": "access denied"} - community.title = input.get("title", "") - community.desc = input.get("desc", "") - community.pic = input.get("pic", "") - community.updatedAt = datetime.now() - session.add(community) - session.commit() - - -@mutation.field("deleteCommunity") -@login_required -async def delete_community(_, info, slug): - auth = info.context["request"].auth - user_id = auth.user_id - - with local_session() as session: - community = session.query(Community).filter(Community.slug == slug).first() - if not community: - return {"error": "invalid community slug"} - if community.owner != user_id: - return {"error": "access denied"} - community.deletedAt = datetime.now() - session.add(community) - session.commit() - - return {} - - -@query.field("getCommunity") -async def get_community(_, info, slug): - with local_session() as session: - community = session.query(Community).filter(Community.slug == slug).first() - if not community: - return {"error": "invalid community id"} - - return community - - -@query.field("getCommunities") -async def get_communities(_, info): - with local_session() as session: - communities = session.query(Community) - return communities - - -def community_follow(user, slug): - with local_session() as session: - cf = CommunityFollower.create(follower=user.slug, community=slug) - session.add(cf) - session.commit() - - -def community_unfollow(user, slug): - with local_session() as session: - following = ( - session.query(CommunityFollower) - .filter( - and_( - CommunityFollower.follower == user.slug, - CommunityFollower.community == slug, - ) - ) - .first() - ) - if not following: - raise Exception("[orm.community] following was not exist") - session.delete(following) - session.commit() - - -@query.field("userFollowedCommunities") -def get_followed_communities(_, _info, user_slug) -> List[Community]: - return followed_communities(user_slug) - - -def followed_communities(user_slug) -> List[Community]: - ccc = [] - with local_session() as session: - ccc = ( - session.query(Community.slug) - .join(CommunityFollower) - .where(CommunityFollower.follower == user_slug) - .all() - ) - return ccc diff --git a/resolvers/editor.py b/resolvers/editor.py index 06abd34d..a44e15f2 100644 --- a/resolvers/editor.py +++ b/resolvers/editor.py @@ -73,9 +73,14 @@ async def update_shout(_, info, inp): shout.update(inp) shout.updatedAt = datetime.now() session.add(shout) - for topic in inp.get("topic_slugs", []): - st = ShoutTopic.create(shout=slug, topic=topic) - session.add(st) + if inp.get("topics"): + # remove old links + links = session.query(ShoutTopic).where(ShoutTopic.shout == slug).all() + for topiclink in links: + session.delete(topiclink) + # add new topic links + for topic in inp.get("topics", []): + ShoutTopic.create(shout=slug, topic=topic) session.commit() GitTask(inp, user.username, user.email, "update shout %s" % (slug)) diff --git a/resolvers/feed.py b/resolvers/feed.py deleted file mode 100644 index f53955f4..00000000 --- a/resolvers/feed.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import List - -from sqlalchemy import and_, desc - -from auth.authenticate import login_required -from base.orm import local_session -from base.resolvers import query -from orm.shout import Shout, ShoutAuthor, ShoutTopic -from orm.topic import TopicFollower -from orm.user import AuthorFollower -from services.zine.shoutscache import prepare_shouts - - -@query.field("shoutsForFeed") -@login_required -async def get_user_feed(_, info, offset, limit) -> List[Shout]: - user = info.context["request"].user - shouts = [] - with local_session() as session: - shouts = ( - session.query(Shout) - .join(ShoutAuthor) - .join(AuthorFollower) - .where(AuthorFollower.follower == user.slug) - .order_by(desc(Shout.createdAt)) - ) - topic_rows = ( - session.query(Shout) - .join(ShoutTopic) - .join(TopicFollower) - .where(TopicFollower.follower == user.slug) - .order_by(desc(Shout.createdAt)) - ) - shouts = shouts.union(topic_rows).limit(limit).offset(offset).all() - return shouts - - -@query.field("recentCandidates") -@login_required -async def user_unpublished_shouts(_, info, offset, limit) -> List[Shout]: - user = info.context["request"].user - with local_session() as session: - shouts = prepare_shouts( - session.query(Shout) - .join(ShoutAuthor) - .where(and_(Shout.publishedAt.is_(None), ShoutAuthor.user == user.slug)) - .order_by(desc(Shout.createdAt)) - .group_by(Shout.id) - .limit(limit) - .offset(offset) - .all() - ) - return shouts diff --git a/resolvers/inbox/chats.py b/resolvers/inbox/chats.py index e6923811..1b80c2ea 100644 --- a/resolvers/inbox/chats.py +++ b/resolvers/inbox/chats.py @@ -4,8 +4,7 @@ from datetime import datetime from auth.authenticate import login_required from base.redis import redis -from base.resolvers import mutation, query -from resolvers.inbox.load import load_messages, load_user_chats +from base.resolvers import mutation async def add_user_to_chat(user_slug: str, chat_id: str, chat=None): @@ -20,40 +19,6 @@ async def add_user_to_chat(user_slug: str, chat_id: str, chat=None): await redis.execute("SET", f"chats_by_user/{member}", json.dumps(chats_ids)) -@query.field("loadChats") -@login_required -async def load_chats(_, info): - user = info.context["request"].user - return await load_user_chats(user.slug) - - -@mutation.field("enterChat") -@login_required -async def enter_chat(_, info, chat_id: str): - ''' enter to public chat with :chat_id ''' - user = info.context["request"].user - chat = await redis.execute("GET", f"chats/{chat_id}") - if not chat: - return { - "error": "chat not exist" - } - else: - chat = dict(json.loads(chat)) - if chat['private']: - return { - "error": "cannot enter private chat" - } - if user.slug not in chat["users"]: - chat["users"].append(user.slug) - await add_user_to_chat(user.slug, chat_id, chat) - await redis.execute("SET" f"chats/{chat_id}", json.dumps(chat)) - chat['messages'] = await load_messages(chat_id) - return { - "chat": chat, - "error": None - } - - @mutation.field("inviteChat") async def invite_to_chat(_, info, invited: str, chat_id: str): ''' invite user with :slug to chat with :chat_id ''' diff --git a/resolvers/inbox/load.py b/resolvers/inbox/load.py index 49c1e766..05431c68 100644 --- a/resolvers/inbox/load.py +++ b/resolvers/inbox/load.py @@ -1,6 +1,8 @@ import json - +from datetime import datetime, timedelta from base.redis import redis +from base.resolvers import query +from auth.authenticate import login_required async def get_unread_counter(chat_id: str, user_slug: str): @@ -24,23 +26,6 @@ async def get_total_unread_counter(user_slug: str): return unread -async def load_user_chats(slug, offset: int, amount: int): - """ load :amount chats of :slug user with :offset """ - - chats = await redis.execute("GET", f"chats_by_user/{slug}") - if chats: - chats = list(json.loads(chats))[offset:offset + amount] - if not chats: - chats = [] - for c in chats: - c['messages'] = await load_messages(c['id']) - c['unread'] = await get_unread_counter(c['id'], slug) - return { - "chats": chats, - "error": None - } - - async def load_messages(chatId: str, offset: int, amount: int): ''' load :amount messages for :chatId with :offset ''' messages = [] @@ -57,3 +42,61 @@ async def load_messages(chatId: str, offset: int, amount: int): "messages": messages, "error": None } + + +@query.field("loadChats") +@login_required +async def load_chats(_, info, offset: int, amount: int): + """ load :amount chats of current user with :offset """ + user = info.context["request"].user + chats = await redis.execute("GET", f"chats_by_user/{user.slug}") + if chats: + chats = list(json.loads(chats))[offset:offset + amount] + if not chats: + chats = [] + for c in chats: + c['messages'] = await load_messages(c['id']) + c['unread'] = await get_unread_counter(c['id'], user.slug) + return { + "chats": chats, + "error": None + } + + +@query.field("loadMessagesBy") +@login_required +async def load_messages_by(_, info, by, offset: int = 0, amount: int = 50): + ''' load :amount messages of :chat_id with :offset ''' + user = info.context["request"].user + my_chats = await redis.execute("GET", f"chats_by_user/{user.slug}") + chat_id = by.get('chat') + if chat_id: + chat = await redis.execute("GET", f"chats/{chat_id}") + if not chat: + return { + "error": "chat not exist" + } + messages = await load_messages(chat_id, offset, amount) + user_id = by.get('author') + if user_id: + chats = await redis.execute("GET", f"chats_by_user/{user_id}") + our_chats = list(set(chats) & set(my_chats)) + for c in our_chats: + messages += await load_messages(c, offset, amount) + body_like = by.get('body') + if body_like: + for c in my_chats: + mmm = await load_messages(c, offset, amount) + for m in mmm: + if body_like in m["body"]: + messages.append(m) + days = by.get("days") + if days: + messages = filter( + lambda m: datetime.now() - int(m["createdAt"]) < timedelta(days=by.get("days")), + messages + ) + return { + "messages": messages, + "error": None + } diff --git a/resolvers/inbox/messages.py b/resolvers/inbox/messages.py index e6494808..d18d2af0 100644 --- a/resolvers/inbox/messages.py +++ b/resolvers/inbox/messages.py @@ -4,25 +4,8 @@ from datetime import datetime from auth.authenticate import login_required from base.redis import redis -from base.resolvers import mutation, query, subscription +from base.resolvers import mutation, subscription from services.inbox import ChatFollowing, MessageResult, MessagesStorage -from resolvers.inbox.load import load_messages - - -@query.field("loadMessages") -@login_required -async def load_chat_messages(_, info, chat_id: str, offset: int = 0, amount: int = 50): - ''' load [amount] chat's messages with [offset] ''' - chat = await redis.execute("GET", f"chats/{chat_id}") - if not chat: - return { - "error": "chat not exist" - } - messages = await load_messages(chat_id, offset, amount) - return { - "messages": messages, - "error": None - } @mutation.field("createMessage") diff --git a/resolvers/inbox/search.py b/resolvers/inbox/search.py index 82048e00..2009fee2 100644 --- a/resolvers/inbox/search.py +++ b/resolvers/inbox/search.py @@ -41,39 +41,3 @@ async def search_users(_, info, query: str, offset: int = 0, amount: int = 50): "slugs": list(result), "error": None } - - -@query.field("searchChats") -@login_required -async def search_chats(_, info, query: str, offset: int = 0, amount: int = 50): - user = info.context["request"].user - my_chats = await redis.execute("GET", f"/chats_by_user/{user.slug}") - chats = [] - for chat_id in my_chats: - chat = await redis.execute("GET", f"chats/{chat_id}") - if chat: - chat = dict(json.loads(chat)) - chats.append(chat) - return { - "chats": chats, - "error": None - } - - -@query.field("searchMessages") -@login_required -async def search_messages(_, info, query: str, offset: int = 0, amount: int = 50): - user = info.context["request"].user - my_chats = await redis.execute("GET", f"/chats_by_user/{user.slug}") - chats = [] - if my_chats: - my_chats = list(json.loads(my_chats)) - for chat_id in my_chats: - chat = await redis.execute("GET", f"chats/{chat_id}") - if chat: - chat = dict(json.loads(chat)) - chats.append(chat) - return { - "chats": chats, - "error": None - } diff --git a/resolvers/profile.py b/resolvers/profile.py index bfd79dd7..201e909d 100644 --- a/resolvers/profile.py +++ b/resolvers/profile.py @@ -1,20 +1,20 @@ from typing import List - -from sqlalchemy import and_, desc, func +from datetime import datetime, timedelta +from sqlalchemy import and_, func from sqlalchemy.orm import selectinload from auth.authenticate import login_required from base.orm import local_session from base.resolvers import mutation, query from orm.reaction import Reaction -from orm.shout import Shout from orm.topic import Topic, TopicFollower from orm.user import AuthorFollower, Role, User, UserRating, UserRole from services.auth.users import UserStorage from services.stat.reacted import ReactedStorage -from services.zine.shoutscache import ShoutsCache +from services.stat.topicstat import TopicStat +from services.zine.shoutauthor import ShoutAuthorStorage -from .community import followed_communities +# from .community import followed_communities from .inbox.load import get_total_unread_counter from .topics import get_topic_stat @@ -25,7 +25,7 @@ async def user_subscriptions(slug: str): "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 ReactedStorage.get_shouts_by_author(slug), - "communities": [c.slug for c in followed_communities(slug)], # communities + # "communities": [c.slug for c in followed_communities(slug)], # communities } @@ -46,24 +46,6 @@ async def get_author_stat(slug): } -@query.field("userReactedShouts") -async def get_user_reacted_shouts(_, slug: str, offset: int, limit: int) -> List[Shout]: - user = await UserStorage.get_user_by_slug(slug) - if not user: - return [] - with local_session() as session: - shouts = ( - session.query(Shout) - .join(Reaction) - .where(Reaction.createdBy == user.slug) - .order_by(desc(Reaction.createdAt)) - .limit(limit) - .offset(offset) - .all() - ) - return shouts - - @query.field("userFollowedTopics") @login_required async def get_followed_topics(_, info, slug) -> List[Topic]: @@ -115,20 +97,7 @@ async def user_followers(_, _info, slug) -> List[User]: return users -@query.field("getUsersBySlugs") -async def get_users_by_slugs(_, _info, slugs): - with local_session() as session: - users = ( - session.query(User) - .options(selectinload(User.ratings)) - .filter(User.slug in slugs) - .all() - ) - return users - - -@query.field("getUserRoles") -async def get_user_roles(_, _info, slug): +async def get_user_roles(slug): with local_session() as session: user = session.query(User).where(User.slug == slug).first() roles = ( @@ -206,7 +175,7 @@ def author_unfollow(user, slug): @query.field("authorsAll") async def get_authors_all(_, _info): users = await UserStorage.get_all_users() - authorslugs = await ShoutsCache.get_all_authors_slugs() + authorslugs = ShoutAuthorStorage.shouts_by_author.keys() authors = [] for author in users: if author.slug in authorslugs: @@ -215,13 +184,32 @@ async def get_authors_all(_, _info): return authors -@query.field("topAuthors") -def get_top_authors(_, _info, offset, limit): - return list(UserStorage.get_top_users())[offset : offset + limit] # type: ignore - - -@query.field("getAuthor") -async def get_author(_, _info, slug): - a = await UserStorage.get_user_by_slug(slug) - a.stat = await get_author_stat(slug) - return a +@query.field("loadAuthorsBy") +async def load_authors_by(_, info, by, amount, offset): + authors = [] + with local_session() as session: + aq = session.query(User) + if by.get("slug"): + aq = aq.filter(User.slug.ilike(f"%{by['slug']}%")) + elif by.get("name"): + aq = aq.filter(User.name.ilike(f"%{by['name']}%")) + elif by.get("topic"): + aaa = list(map(lambda a: a.slug, TopicStat.authors_by_topic.get(by["topic"]))) + aq = aq.filter(User.name._in(aaa)) + if by.get("lastSeen"): # in days + days_before = datetime.now() - timedelta(days=by["lastSeen"]) + aq = aq.filter(User.lastSeen > days_before) + elif by.get("createdAt"): # in days + days_before = datetime.now() - timedelta(days=by["createdAt"]) + aq = aq.filter(User.createdAt > days_before) + aq = aq.group_by( + User.id + ).order_by( + by.get("order") or "createdAt" + ).limit(amount).offset(offset) + authors = list(map(lambda r: r.User, session.execute(aq))) + if by.get("stat"): + for a in authors: + a.stat = await get_author_stat(a.slug) + authors = list(set(authors)).sort(lambda a: a["stat"].get(by.get("stat"))) + return authors diff --git a/resolvers/reactions.py b/resolvers/reactions.py index f1b789ad..1f083cd0 100644 --- a/resolvers/reactions.py +++ b/resolvers/reactions.py @@ -1,6 +1,6 @@ -from datetime import datetime - -from sqlalchemy import and_, desc +from datetime import datetime, timedelta +from sqlalchemy import and_, desc, select +from sqlalchemy.orm import selectinload from auth.authenticate import login_required from base.orm import local_session @@ -8,14 +8,12 @@ from base.resolvers import mutation, query from orm.reaction import Reaction, ReactionKind from orm.shout import Shout, ShoutReactionsFollower from orm.user import User -from services.auth.users import UserStorage from services.stat.reacted import ReactedStorage -from services.stat.viewed import ViewedStorage async def get_reaction_stat(reaction_id): return { - "viewed": await ViewedStorage.get_reaction(reaction_id), + # "viewed": await ViewedStorage.get_reaction(reaction_id), "reacted": len(await ReactedStorage.get_reaction(reaction_id)), "rating": await ReactedStorage.get_reaction_rating(reaction_id), "commented": len(await ReactedStorage.get_reaction_comments(reaction_id)), @@ -202,57 +200,55 @@ async def delete_reaction(_, info, rid): return {} -@query.field("reactionsForShouts") -async def get_reactions_for_shouts(_, info, shouts, offset, limit): - return await reactions_for_shouts(shouts, offset, limit) +@query.field("loadReactionsBy") +async def load_reactions_by(_, info, by, amount=50, offset=0): + """ + :param by: { + shout: 'some-slug' + author: 'discours', + topic: 'culture', + body: 'something else', + stat: 'rating' | 'comments' | 'reacted' | 'views', + days: 30 + } + :param amount: int amount of shouts + :param offset: int offset in this order + :return: Reaction[] + """ + q = select(Reaction).options( + selectinload(Reaction.shout), + ).where( + Reaction.deletedAt.is_(None) + ).join( + Shout, + Shout.slug == Reaction.shout + ) + if by.get("slug"): + q = q.filter(Shout.slug == by["slug"]) + else: + if by.get("reacted"): + user = info.context["request"].user + q = q.filter(Reaction.createdBy == user.slug) + if by.get("author"): + q = q.filter(Reaction.createdBy == by["author"]) + if by.get("topic"): + q = q.filter(Shout.topics.contains(by["topic"])) + if by.get("body"): + q = q.filter(Reaction.body.ilike(f'%{by["body"]}%')) + if by.get("days"): + before = datetime.now() - timedelta(days=int(by["days"]) or 30) + q = q.filter(Reaction.createdAt > before) + q = q.group_by(Shout.id).order_by( + desc(by.get("order") or "createdAt") + ).limit(amount).offset(offset) -async def reactions_for_shouts(shouts, offset, limit): - reactions = [] + rrr = [] with local_session() as session: - for slug in shouts: - reactions += ( - session.query(Reaction) - .filter(Reaction.shout == slug) - .where(Reaction.deletedAt.is_not(None)) - .order_by(desc("createdAt")) - .offset(offset) - .limit(limit) - .all() - ) - for r in reactions: - r.stat = await get_reaction_stat(r.id) - r.createdBy = await UserStorage.get_user(r.createdBy or "discours") - return reactions - reactions = [] - with local_session() as session: - for slug in shouts: - reactions += ( - session.query(Reaction) - .filter(Reaction.shout == slug) - .where(Reaction.deletedAt.is_not(None)) - .order_by(desc("createdAt")) - .offset(offset) - .limit(limit) - .all() - ) - for r in reactions: - r.stat = await get_reaction_stat(r.id) - r.createdBy = await UserStorage.get_user(r.createdBy or "discours") - return reactions - - -@query.field("reactionsByAuthor") -async def get_reactions_by_author(_, info, slug, limit=50, offset=0): - reactions = [] - with local_session() as session: - reactions = ( - session.query(Reaction) - .where(Reaction.createdBy == slug) - .limit(limit) - .offset(offset) - ) - for r in reactions: - r.stat = await get_reaction_stat(r.id) - r.createdBy = await UserStorage.get_user(r.createdBy or "discours") - return reactions + # post query stats and author's captions + for r in list(map(lambda r: r.Reaction, session.execute(q))): + r.stat = await get_reaction_stat(r.id) + rrr.append(r) + if by.get("stat"): + rrr.sort(lambda r: r.stat.get(by["stat"]) or r.createdAt) + return rrr diff --git a/resolvers/topics.py b/resolvers/topics.py index 12d914f8..b5824fc0 100644 --- a/resolvers/topics.py +++ b/resolvers/topics.py @@ -6,11 +6,10 @@ from auth.authenticate import login_required from base.orm import local_session from base.resolvers import mutation, query from orm.topic import Topic, TopicFollower -from services.zine.shoutscache import ShoutsCache from services.zine.topics import TopicStorage from services.stat.reacted import ReactedStorage from services.stat.topicstat import TopicStat -from services.stat.viewed import ViewedStorage +# from services.stat.viewed import ViewedStorage async def get_topic_stat(slug): @@ -18,7 +17,7 @@ async def get_topic_stat(slug): "shouts": len(TopicStat.shouts_by_topic.get(slug, {}).keys()), "authors": len(TopicStat.authors_by_topic.get(slug, {}).keys()), "followers": len(TopicStat.followers_by_topic.get(slug, {}).keys()), - "viewed": await ViewedStorage.get_topic(slug), + # "viewed": await ViewedStorage.get_topic(slug), "reacted": len(await ReactedStorage.get_topic(slug)), "commented": len(await ReactedStorage.get_topic_comments(slug)), "rating": await ReactedStorage.get_topic_rating(slug) @@ -43,7 +42,7 @@ async def topics_by_community(_, info, community): @query.field("topicsByAuthor") async def topics_by_author(_, _info, author): - shouts = ShoutsCache.by_author.get(author, []) + shouts = TopicStorage.get_topics_by_author(author) author_topics = set() for s in shouts: for tpc in s.topics: diff --git a/resolvers/zine.py b/resolvers/zine.py index 29adf225..cea5df37 100644 --- a/resolvers/zine.py +++ b/resolvers/zine.py @@ -1,249 +1,84 @@ -from graphql.type import GraphQLResolveInfo from datetime import datetime, timedelta from sqlalchemy.orm import selectinload -from sqlalchemy.sql.expression import and_, desc, select +from sqlalchemy.sql.expression import desc, select from auth.authenticate import login_required from base.orm import local_session from base.resolvers import mutation, query -from orm.collection import ShoutCollection -from orm.shout import Shout, ShoutTopic -from orm.topic import Topic -from resolvers.community import community_follow, community_unfollow +from orm.shout import Shout +from orm.reaction import Reaction +# from resolvers.community import community_follow, community_unfollow from resolvers.profile import author_follow, author_unfollow from resolvers.reactions import reactions_follow, reactions_unfollow from resolvers.topics import topic_follow, topic_unfollow -from services.search import SearchService -from services.stat.viewed import ViewedStorage from services.zine.shoutauthor import ShoutAuthorStorage -from services.zine.shoutscache import ShoutsCache, get_shout_stat +from services.stat.reacted import ReactedStorage -@mutation.field("incrementView") -async def increment_view(_, _info, shout): - # TODO: use ackee to collect views - async with ViewedStorage.lock: - return ViewedStorage.increment(shout) +@query.field("loadShoutsBy") +async def load_shouts_by(_, info, by, amount=50, offset=0): + """ + :param by: { + layout: 'audio', + published: true, + author: 'discours', + topic: 'culture', + title: 'something', + body: 'something else', + stat: 'rating' | 'comments' | 'reacted' | 'views', + days: 30 + } + :param amount: int amount of shouts + :param offset: int offset in this order + :return: Shout[] + """ - -@query.field("topMonth") -async def top_month(_, _info, offset, limit): - async with ShoutsCache.lock: - return ShoutsCache.top_month[offset : offset + limit] - - -@query.field("topPublished") -async def top_published(_, _info, daysago, offset, limit): - async with ShoutsCache.lock: - return ShoutsCache.get_top_published_before(daysago, offset, limit) - - -@query.field("topCommented") -async def top_commented(_, _info, offset, limit): - async with ShoutsCache.lock: - return ShoutsCache.top_commented[offset : offset + limit] - - -@query.field("topOverall") -async def top_overall(_, _info, offset, limit): - async with ShoutsCache.lock: - return ShoutsCache.top_overall[offset : offset + limit] - - -@query.field("recentPublished") -async def recent_published(_, _info, offset, limit): - async with ShoutsCache.lock: - return ShoutsCache.recent_published[offset : offset + limit] - - -@query.field("recentAll") -async def recent_all(_, _info, offset, limit): - async with ShoutsCache.lock: - return ShoutsCache.recent_all[offset : offset + limit] - - -@query.field("recentReacted") -async def recent_reacted(_, _info, offset, limit): - async with ShoutsCache.lock: - return ShoutsCache.recent_reacted[offset : offset + limit] - - -@query.field("recentCommented") -async def recent_commented(_, _info, offset, limit): - async with ShoutsCache.lock: - return ShoutsCache.recent_commented[offset : offset + limit] - - -@query.field("getShoutBySlug") -async def get_shout_by_slug(_, info, slug): - all_fields = [ - node.name.value for node in info.field_nodes[0].selection_set.selections - ] - selected_fields = set(["authors", "topics"]).intersection(all_fields) - select_options = [selectinload(getattr(Shout, field)) for field in selected_fields] - with local_session() as session: - # s = text(open("src/queries/shout-by-slug.sql", "r").read() % slug) - shout = ( - session.query(Shout) - .options(select_options) - .filter(Shout.slug == slug) - .first() - ) - - if not shout: - print(f"shout with slug {slug} not exist") - return {"error": "shout not found"} - else: - for a in shout.authors: - a.caption = await ShoutAuthorStorage.get_author_caption(slug, a.slug) - return shout - - -@query.field("searchQuery") -async def get_search_results(_, _info, searchtext, offset, limit): - shouts = SearchService.search(searchtext) - # TODO: sort and filter types for search service - for s in shouts: - shout = s.dict() - for a in shout['authors']: - a.caption = await ShoutAuthorStorage.get_author_caption(s.slug, a.slug) - s.stat.relevance = 1 # FIXME: expecting search engine rated relevance - return shouts[offset : offset + limit] - - -@query.field("shoutsByAuthors") -async def shouts_by_authors(_, _info, slugs, offset=0, limit=100): - async with ShoutsCache.lock: - shouts = {} - for author in slugs: - shouts_by_author = list(ShoutsCache.by_author.get(author, {}).values()) - for s in shouts_by_author: - for a in s.authors: - a.caption = await ShoutAuthorStorage.get_author_caption(s.slug, a.slug) - if bool(s.publishedAt): - shouts[s.slug] = s - shouts_prepared = list(shouts.values()) - shouts_prepared.sort(key=lambda s: s.publishedAt, reverse=True) - return shouts_prepared[offset : offset + limit] - - -@query.field("recentLayoutShouts") -async def shouts_by_layout_recent(_param, _info: GraphQLResolveInfo, layout, amount=100, offset=0): - async with ShoutsCache.lock: - shouts = {} - # for layout in ['image', 'audio', 'video', 'literature']: - shouts_by_layout = list(ShoutsCache.by_layout.get(layout, [])) - for s in shouts_by_layout: - if s.visibility == 'public': # if bool(s.publishedAt): - shouts[s.slug] = s - for a in s.authors: - a.caption = await ShoutAuthorStorage.get_author_caption(s.slug, a.slug) - - shouts_prepared = list(shouts.values()) - shouts_prepared.sort(key=lambda s: s.createdAt, reverse=True) - return shouts_prepared[offset : offset + amount] - - -@query.field("topLayoutShouts") -async def shouts_by_layout_top(_param, _info: GraphQLResolveInfo, layout, amount=100, offset=0): - async with ShoutsCache.lock: - shouts = {} - # for layout in ['image', 'audio', 'video', 'literature']: - shouts_by_layout = list(ShoutsCache.by_layout.get(layout, [])) - for s in shouts_by_layout: - if s.visibility == 'public': # if bool(s.publishedAt): - shouts[s.slug] = s - for a in s.authors: - a.caption = await ShoutAuthorStorage.get_author_caption(s.slug, a.slug) - s.stat = await get_shout_stat(s.slug) - shouts_prepared = list(shouts.values()) - shouts_prepared.sort(key=lambda s: s.stat["rating"], reverse=True) - return shouts_prepared[offset : offset + amount] - - -@query.field("topMonthLayoutShouts") -async def shouts_by_layout_topmonth(_param, _info: GraphQLResolveInfo, layout, amount=100, offset=0): - async with ShoutsCache.lock: - shouts = {} - # for layout in ['image', 'audio', 'video', 'literature']: - shouts_by_layout = list(ShoutsCache.by_layout.get(layout, [])) - month_ago = datetime.now() - timedelta(days=30) - for s in shouts_by_layout: - if s.visibility == 'public' and s.createdAt > month_ago: - shouts[s.slug] = s - for a in s.authors: - a.caption = await ShoutAuthorStorage.get_author_caption(s.slug, a.slug) - - shouts_prepared = list(shouts.values()) - shouts_prepared.sort(key=lambda s: s.stat["rating"], reverse=True) - return shouts_prepared[offset : offset + amount] - - -@query.field("shoutsByTopics") -async def shouts_by_topics(_, _info, slugs, offset=0, limit=100): - async with ShoutsCache.lock: - shouts = {} - for topic in slugs: - shouts_by_topic = list(ShoutsCache.by_topic.get(topic, {}).values()) - for s in shouts_by_topic: - for a in s.authors: - a.caption = await ShoutAuthorStorage.get_author_caption(s.slug, a.slug) - if bool(s.publishedAt): - shouts[s.slug] = s - shouts_prepared = list(shouts.values()) - shouts_prepared.sort(key=lambda s: s.publishedAt, reverse=True) - return shouts_prepared[offset : offset + limit] - - -@query.field("shoutsByCollection") -async def shouts_by_collection(_, _info, collection, offset, limit): - with local_session() as session: - shouts = ( - session.query(Shout) - .join(ShoutCollection, ShoutCollection.collection == collection) - .where(and_(ShoutCollection.shout == Shout.slug, Shout.publishedAt.is_not(None))) - .order_by(desc("publishedAt")) - .limit(limit) - .offset(offset) - ) - for s in shouts: - for a in s.authors: - a.caption = await ShoutAuthorStorage.get_author_caption(s.slug, a.slug) - return shouts - - -SINGLE_COMMUNITY = True - - -@query.field("shoutsByCommunities") -async def shouts_by_communities(_, info, slugs, offset, limit): - if SINGLE_COMMUNITY: - return recent_published(_, info, offset, limit) + q = select(Shout, Reaction).options( + selectinload(Shout.authors), + selectinload(Shout.topics), + selectinload(Shout.reactions) + ).where( + Shout.deletedAt.is_(None) + ).join( + Reaction, Reaction.shout == Shout.slug + ) + if by.get("slug"): + q = q.filter(Shout.slug == by["slug"]) else: - with local_session() as session: - # TODO fix postgres high load - shouts = ( - session.query(Shout) - .distinct() - .join(ShoutTopic) - .where( - and_( - Shout.publishedAt.is_not(None), - ShoutTopic.topic.in_( - select(Topic.slug).where(Topic.community.in_(slugs)) - ), - ) - ) - .order_by(desc("publishedAt")) - .limit(limit) - .offset(offset) - ) + if by.get("reacted"): + user = info.context["request"].user + q = q.filter(Reaction.createdBy == user.slug) + if by.get("published"): + q = q.filter(Shout.publishedAt.is_not(None)) + if by.get("layout"): + q = q.filter(Shout.layout == by["layout"]) + if by.get("author"): + q = q.filter(Shout.authors.contains(by["author"])) + if by.get("topic"): + q = q.filter(Shout.topics.contains(by["topic"])) + if by.get("title"): + q = q.filter(Shout.title.ilike(f'%{by["title"]}%')) + if by.get("body"): + q = q.filter(Shout.body.ilike(f'%{by["body"]}%')) + if by.get("days"): + before = datetime.now() - timedelta(days=int(by["days"]) or 30) + q = q.filter(Shout.createdAt > before) + q = q.group_by(Shout.id).order_by( + desc(by.get("order") or "createdAt") + ).limit(amount).offset(offset) - for s in shouts: + shouts = [] + with local_session() as session: + # post query stats and author's captions + for s in list(map(lambda r: r.Shout, session.execute(q))): + s.stat = await ReactedStorage.get_shout_stat(s.slug) for a in s.authors: a.caption = await ShoutAuthorStorage.get_author_caption(s.slug, a.slug) - return shouts + shouts.append(s) + if by.get("stat"): + shouts.sort(lambda s: s.stat.get(by["stat"]) or s.createdAt) + return shouts @mutation.field("follow") @@ -256,7 +91,8 @@ async def follow(_, info, what, slug): elif what == "TOPIC": topic_follow(user, slug) elif what == "COMMUNITY": - community_follow(user, slug) + # community_follow(user, slug) + pass elif what == "REACTIONS": reactions_follow(user, slug) except Exception as e: @@ -276,7 +112,8 @@ async def unfollow(_, info, what, slug): elif what == "TOPIC": topic_unfollow(user, slug) elif what == "COMMUNITY": - community_unfollow(user, slug) + # community_unfollow(user, slug) + pass elif what == "REACTIONS": reactions_unfollow(user, slug) except Exception as e: diff --git a/schema.graphql b/schema.graphql index 4141ca39..43437974 100644 --- a/schema.graphql +++ b/schema.graphql @@ -110,18 +110,6 @@ input ProfileInput { bio: String } -input CommunityInput { - title: String! - desc: String - pic: String -} - -input CollectionInput { - title: String! - desc: String - pic: String -} - input TopicInput { slug: String! community: String! @@ -161,7 +149,7 @@ type Mutation { updateChat(chat: ChatInput!): Result! deleteChat(chatId: String!): Result! inviteChat(chatId: String!, userslug: String!): Result! - enterChat(chatId: String!): Result! + createMessage(chatId: String!, body: String!, replyTo: String): Result! updateMessage(chatId: String!, id: Int!, body: String!): Result! deleteMessage(chatId: String!, id: Int!): Result! @@ -180,7 +168,7 @@ type Mutation { # user profile rateUser(slug: String!, value: Int!): Result! - # updateOnlineStatus: Result! + updateOnlineStatus: Result! updateProfile(profile: ProfileInput!): Result! # topics @@ -189,22 +177,11 @@ type Mutation { updateTopic(input: TopicInput!): Result! destroyTopic(slug: String!): Result! - # reactions createReaction(reaction: ReactionInput!): Result! updateReaction(reaction: ReactionInput!): Result! deleteReaction(id: Int!): Result! - # community - createCommunity(community: CommunityInput!): Result! - updateCommunity(community: CommunityInput!): Result! - deleteCommunity(slug: String!): Result! - - # collection - createCollection(collection: CollectionInput!): Result! - updateCollection(collection: CollectionInput!): Result! - deleteCollection(slug: String!): Result! - # collab inviteAuthor(author: String!, shout: String!): Result! removeAuthor(author: String!, shout: String!): Result! @@ -212,65 +189,70 @@ type Mutation { # following follow(what: FollowingEntity!, slug: String!): Result! unfollow(what: FollowingEntity!, slug: String!): Result! - - # seen - incrementView(shout: String!): Result! } +input MessagesBy { + author: String + body: String + chat: String + days: Int +} + +input AuthorsBy { + lastSeen: DateTime + createdAt: DateTime + stat: String + slug: String + name: String + topic: String +} + +input ShoutsBy { + slug: String + title: String + body: String + topic: String + author: String + days: Int + layout: String + published: Boolean + visibility: String + stat: String +} + +input ReactionBy { + shout: String + body: String + topic: String + author: String + days: Int + stat: String +} ################################### Query type Query { # inbox - loadChats(offset: Int, amount: Int): Result! - loadMessages(chatId: String!, offset: Int, amount: Int): Result! - searchUsers(q: String!, offset: Int, amount: Int): Result! - searchChats(q: String!, offset: Int, amount: Int): Result! - searchMessages(q: String!, offset: Int, amount: Int): Result! + loadChats(offset: Int, amount: Int): Result! # your chats + loadMessagesBy(by: MessagesBy!, amount: Int, offset: Int): Result! + searchUsers(query: String!, amount: Int, offset: Int): Result! # auth isEmailUsed(email: String!): Boolean! signIn(email: String!, password: String, lang: String): AuthResult! signOut: AuthResult! - # profile - getUsersBySlugs(slugs: [String]!): [Author]! + # zine + loadAuthorsBy(by: AuthorsBy, amount: Int, offset: Int): [Author]! + loadShoutsBy(by: ShoutsBy, amount: Int, offset: Int): [Shout]! + loadReactionsBy(by: ReactionBy!, amount: Int, limit: Int): [Reaction]! userFollowers(slug: String!): [Author]! userFollowedAuthors(slug: String!): [Author]! userFollowedTopics(slug: String!): [Topic]! - userFollowedCommunities(slug: String!): [Community]! - userReactedShouts(slug: String!): [Shout]! # test - getUserRoles(slug: String!): [Role]! + + authorsAll: [Author]! getAuthor(slug: String!): User! - # shouts - getShoutBySlug(slug: String!): Shout! - shoutsForFeed(offset: Int!, limit: Int!): [Shout]! # test - shoutsByLayout(layout: String, amount: Int!, offset: Int!): [Shout]! - shoutsByTopics(slugs: [String]!, offset: Int!, limit: Int!): [Shout]! - shoutsByAuthors(slugs: [String]!, offset: Int!, limit: Int!): [Shout]! - shoutsByCommunities(slugs: [String]!, offset: Int!, limit: Int!): [Shout]! - # topReacted(offset: Int!, limit: Int!): [Shout]! - topAuthors(offset: Int!, limit: Int!): [Author]! # by User.rating - topPublished(daysago: Int!, offset: Int!, limit: Int!): [Shout]! - topMonth(offset: Int!, limit: Int!): [Shout]! # TODO: implement topPublishedAfter(day, offset, limit) - topOverall(offset: Int!, limit: Int!): [Shout]! - topCommented(offset: Int!, limit: Int!): [Shout]! - recentPublished(offset: Int!, limit: Int!): [Shout]! # homepage - recentReacted(offset: Int!, limit: Int!): [Shout]! # TODO: use in design! - recentCommented(offset: Int!, limit: Int!): [Shout]! - recentAll(offset: Int!, limit: Int!): [Shout]! - recentCandidates(offset: Int!, limit: Int!): [Shout]! - - # expo - topMonthLayoutShouts(layout: String!, amount: Int, offset: Int): [Shout]! - topLayoutShouts(layout: String!, amount: Int, offset: Int): [Shout]! - recentLayoutShouts(layout: String!, amount: Int, offset: Int): [Shout]! - - # reactons - reactionsByAuthor(slug: String!, offset: Int!, limit: Int!): [Reaction]! - reactionsForShouts(shouts: [String]!, offset: Int!, limit: Int!): [Reaction]! - # collab getCollabs: [Collab]! @@ -283,18 +265,6 @@ type Query { topicsRandom(amount: Int): [Topic]! topicsByCommunity(community: String!): [Topic]! topicsByAuthor(author: String!): [Topic]! - - # collections - collectionsAll: [Collection]! - getUserCollections(author: String!): [Collection]! - shoutsByCollection(collection: String!, offset: Int!, limit: Int!): [Shout]! - - # communities - getCommunity(slug: String): Community! - getCommunities: [Community]! # all - - # search - searchQuery(q: String, offset: Int!, limit: Int!): [Shout] } ############################################ Subscription diff --git a/services/stat/reacted.py b/services/stat/reacted.py index e6ec7b37..de854f6a 100644 --- a/services/stat/reacted.py +++ b/services/stat/reacted.py @@ -32,6 +32,16 @@ class ReactedStorage: lock = asyncio.Lock() modified_shouts = set([]) + @staticmethod + async def get_shout_stat(slug): + return { + # TODO: use ackee as datasource + "viewed": 0, # await ViewedStorage.get_shout(slug), + "reacted": len(await ReactedStorage.get_shout(slug)), + "commented": len(await ReactedStorage.get_comments(slug)), + "rating": await ReactedStorage.get_rating(slug), + } + @staticmethod async def get_shout(shout_slug): self = ReactedStorage diff --git a/services/zine/shoutscache.py b/services/zine/shoutscache.py deleted file mode 100644 index 442c09ab..00000000 --- a/services/zine/shoutscache.py +++ /dev/null @@ -1,285 +0,0 @@ -import asyncio -from datetime import datetime, timedelta - -from sqlalchemy import and_, desc, func, select -from sqlalchemy.orm import selectinload - -from base.orm import local_session -from orm.reaction import Reaction, ReactionKind -from orm.shout import Shout -from services.stat.reacted import ReactedStorage - - -async def get_shout_stat(slug): - return { - # TODO: use ackee as datasource - "viewed": 0, # await ViewedStorage.get_shout(slug), - "reacted": len(await ReactedStorage.get_shout(slug)), - "commented": len(await ReactedStorage.get_comments(slug)), - "rating": await ReactedStorage.get_rating(slug), - } - - -async def prepare_shouts(session, stmt): - shouts = [] - print(stmt) - for s in list(map(lambda r: r.Shout, session.execute(stmt))): - s.stat = await get_shout_stat(s.slug) - shouts.append(s) - return shouts - - -LAYOUTS = ['audio', 'video', 'image', 'literature'] - - -class ShoutsCache: - # limit = 200 - period = 60 * 60 # 1 hour - lock = asyncio.Lock() - - recent_published = [] - recent_all = [] - recent_reacted = [] - recent_commented = [] - top_month = [] - top_overall = [] - top_commented = [] - - by_author = {} - by_topic = {} - by_layout = {} - - @staticmethod - async def prepare_recent_published(): - with local_session() as session: - shouts = await prepare_shouts( - session, - ( - select(Shout) - .options( - selectinload(Shout.authors), - selectinload(Shout.topics) - ) - .where(Shout.deletedAt.is_(None)) - .filter(Shout.publishedAt.is_not(None)) - .group_by(Shout.id) - .order_by(desc("publishedAt")) - # .limit(ShoutsCache.limit) - ), - ) - async with ShoutsCache.lock: - for s in shouts: - for a in s.authors: - ShoutsCache.by_author[a.slug] = ShoutsCache.by_author.get(a.slug, {}) - ShoutsCache.by_author[a.slug][s.slug] = s - for t in s.topics: - ShoutsCache.by_topic[t.slug] = ShoutsCache.by_topic.get(t.slug, {}) - ShoutsCache.by_topic[t.slug][s.slug] = s - if s.layout in LAYOUTS: - ShoutsCache.by_layout[s.layout] = ShoutsCache.by_layout.get(s.layout, []) - ShoutsCache.by_layout[s.layout].append(s) - print("[zine.cache] indexed by %d topics " % len(ShoutsCache.by_topic.keys())) - print("[zine.cache] indexed by %d authors " % len(ShoutsCache.by_author.keys())) - print("[zine.cache] indexed by %d layouts " % len(ShoutsCache.by_layout.keys())) - ShoutsCache.recent_published = shouts - print("[zine.cache] %d recently published shouts " % len(shouts)) - - @staticmethod - async def prepare_recent_all(): - with local_session() as session: - shouts = await prepare_shouts( - session, - ( - select(Shout) - .options( - selectinload(Shout.authors), - selectinload(Shout.topics) - ) - .where(Shout.deletedAt.is_(None)) - .group_by(Shout.id) - .order_by(desc("createdAt")) - # .limit(ShoutsCache.limit) - ) - ) - async with ShoutsCache.lock: - ShoutsCache.recent_all = shouts - print("[zine.cache] %d recently created shouts " % len(ShoutsCache.recent_all)) - - @staticmethod - async def prepare_recent_reacted(): - with local_session() as session: - reactions = session.query(Reaction).order_by(Reaction.createdAt).all() - # .limit(ShoutsCache.limit) - reacted_slugs = set([]) - for r in reactions: - reacted_slugs.add(r.shout) - shouts = await prepare_shouts( - session, - ( - select( - Shout, - Reaction.createdAt.label('reactedAt') - ) - .options( - selectinload(Shout.authors), - selectinload(Shout.topics), - selectinload(Shout.reactions), - ) - .join(Reaction) - .where(and_(Shout.deletedAt.is_(None), Shout.slug.in_(reacted_slugs))) - .filter(Shout.publishedAt.is_not(None)) - .group_by(Shout.id, "reactedAt") - .order_by(desc("reactedAt")) - # .limit(ShoutsCache.limit) - ) - ) - async with ShoutsCache.lock: - ShoutsCache.recent_reacted = shouts - print("[zine.cache] %d recently reacted shouts " % len(shouts)) - - @staticmethod - async def prepare_recent_commented(): - with local_session() as session: - reactions = session.query(Reaction).order_by(Reaction.createdAt).all() - # .limit(ShoutsCache.limit) - commented_slugs = set([]) - for r in reactions: - if r.body and len(r.body) > 0: - commented_slugs.add(r.shout) - shouts = await prepare_shouts( - session, - ( - select( - Shout, - Reaction.createdAt.label('reactedAt') - ) - .options( - selectinload(Shout.authors), - selectinload(Shout.topics), - selectinload(Shout.reactions), - ) - .join(Reaction) - .where(and_(Shout.deletedAt.is_(None), Shout.slug.in_(commented_slugs))) - .group_by(Shout.id, "reactedAt") - .order_by(desc("reactedAt")) - # .limit(ShoutsCache.limit) - ) - ) - async with ShoutsCache.lock: - ShoutsCache.recent_commented = shouts - print("[zine.cache] %d recently commented shouts " % len(shouts)) - - @staticmethod - async def prepare_top_overall(): - with local_session() as session: - shouts = await prepare_shouts( - session, - ( - select( - Shout, - func.sum(Reaction.id).label('reacted') - ) - .options( - selectinload(Shout.authors), - selectinload(Shout.topics), - selectinload(Shout.reactions), - ) - .join(Reaction, Reaction.kind == ReactionKind.LIKE) - .where(Shout.deletedAt.is_(None)) - .filter(Shout.publishedAt.is_not(None)) - .group_by(Shout.id) - .order_by(desc("reacted")) - # .limit(ShoutsCache.limit) - ), - ) - shouts.sort(key=lambda s: s.stat["rating"], reverse=True) - async with ShoutsCache.lock: - print("[zine.cache] %d top rated published " % len(shouts)) - ShoutsCache.top_overall = shouts - - @staticmethod - async def prepare_top_month(): - month_ago = datetime.now() - timedelta(days=30) - with local_session() as session: - shouts = await prepare_shouts( - session, - ( - select(Shout) - .options( - selectinload(Shout.authors), - selectinload(Shout.topics), - selectinload(Shout.reactions), - ) - .join(Reaction) - .where(Shout.deletedAt.is_(None)) - .filter(Shout.publishedAt > month_ago) - .group_by(Shout.id) - # .limit(ShoutsCache.limit) - ), - ) - shouts.sort(key=lambda s: s.stat["rating"], reverse=True) - async with ShoutsCache.lock: - ShoutsCache.top_month = shouts - print("[zine.cache] %d top month published " % len(ShoutsCache.top_month)) - - @staticmethod - async def prepare_top_commented(): - month_ago = datetime.now() - timedelta(days=30) - with local_session() as session: - shouts = await prepare_shouts( - session, - ( - select( - Shout, - func.sum(Reaction.id).label("commented") - ) - .options( - selectinload(Shout.authors), - selectinload(Shout.topics), - selectinload(Shout.reactions) - ) - .join(Reaction, func.length(Reaction.body) > 0) - .where(Shout.deletedAt.is_(None)) - .filter(Shout.publishedAt > month_ago) - .group_by(Shout.id) - .order_by(desc("commented")) - # .limit(ShoutsCache.limit) - ), - ) - shouts.sort(key=lambda s: s.stat["commented"], reverse=True) - async with ShoutsCache.lock: - ShoutsCache.top_commented = shouts - print("[zine.cache] %d last month top commented shouts " % len(ShoutsCache.top_commented)) - - @staticmethod - async def get_top_published_before(daysago, offset, limit): - shouts_by_rating = [] - before = datetime.now() - timedelta(days=daysago) - for s in ShoutsCache.recent_published: - if s.publishedAt >= before: - shouts_by_rating.append(s) - shouts_by_rating.sort(lambda s: s.stat["rating"], reverse=True) - return shouts_by_rating - - @staticmethod - async def get_all_authors_slugs(): - slugs = ShoutsCache.by_author.keys() - return slugs - - @staticmethod - async def worker(): - while True: - try: - await ShoutsCache.prepare_top_month() - await ShoutsCache.prepare_top_overall() - await ShoutsCache.prepare_top_commented() - - await ShoutsCache.prepare_recent_published() - await ShoutsCache.prepare_recent_all() - await ShoutsCache.prepare_recent_reacted() - await ShoutsCache.prepare_recent_commented() - print("[zine.cache] periodical update") - except Exception as err: - print("[zine.cache] error: %s" % (err)) - raise err - await asyncio.sleep(ShoutsCache.period)