diff --git a/resolvers/__init__.py b/resolvers/__init__.py index 25b516b2..fccca3a0 100644 --- a/resolvers/__init__.py +++ b/resolvers/__init__.py @@ -7,18 +7,18 @@ from resolvers.auth import ( auth_send_link, get_current_user, ) -from resolvers.collab import remove_author, invite_author -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 ( +from resolvers.create.collab import remove_author, invite_author +from resolvers.create.migrate import markdown_body +from resolvers.create.editor import create_shout, delete_shout, update_shout + +from resolvers.zine.profile import ( load_authors_by, rate_user, update_profile ) -from resolvers.reactions import ( +from resolvers.zine.reactions import ( create_reaction, delete_reaction, update_reaction, @@ -26,7 +26,7 @@ from resolvers.reactions import ( reactions_follow, load_reactions_by ) -from resolvers.topics import ( +from resolvers.zine.topics import ( topic_follow, topic_unfollow, topics_by_author, @@ -35,9 +35,13 @@ from resolvers.topics import ( get_topic ) -from resolvers.zine import ( +from resolvers.zine.following import ( follow, - unfollow, + unfollow +) + +from resolvers.zine.load import ( + load_shout, load_shouts_by ) @@ -56,9 +60,10 @@ from resolvers.inbox.messages import ( ) from resolvers.inbox.load import ( load_chats, - load_messages_by + load_messages_by, + load_recipients ) -from resolvers.inbox.search import search_users +from resolvers.inbox.search import search_recipients __all__ = [ # auth @@ -69,32 +74,34 @@ __all__ = [ "auth_send_link", "sign_out", "get_current_user", - # authors + # zine.profile "load_authors_by", "rate_user", "update_profile", "get_authors_all", - # zine + # zine.load + "load_shout", "load_shouts_by", + # zine.following "follow", "unfollow", - # editor + # create.editor "create_shout", "update_shout", "delete_shout", - # migrate + # create.migrate "markdown_body", - # collab + # create.collab "invite_author", "remove_author", - # topics + # zine.topics "topics_all", "topics_by_community", "topics_by_author", "topic_follow", "topic_unfollow", "get_topic", - # reactions + # zine.reactions "reactions_follow", "reactions_unfollow", "create_reaction", @@ -113,5 +120,6 @@ __all__ = [ "update_message", "message_generator", "mark_as_read", - "search_users" + "load_recipients", + "search_recipients" ] diff --git a/resolvers/auth.py b/resolvers/auth.py index 64fecf8c..02bff8b1 100644 --- a/resolvers/auth.py +++ b/resolvers/auth.py @@ -17,7 +17,7 @@ from base.exceptions import (BaseHttpException, InvalidPassword, InvalidToken, from base.orm import local_session from base.resolvers import mutation, query from orm import Role, User -from resolvers.profile import user_subscriptions +from resolvers.zine.profile import user_subscriptions @mutation.field("refreshSession") diff --git a/resolvers/collab.py b/resolvers/create/collab.py similarity index 100% rename from resolvers/collab.py rename to resolvers/create/collab.py diff --git a/resolvers/editor.py b/resolvers/create/editor.py similarity index 97% rename from resolvers/editor.py rename to resolvers/create/editor.py index a44e15f2..5dbcf95c 100644 --- a/resolvers/editor.py +++ b/resolvers/create/editor.py @@ -7,7 +7,7 @@ from orm.rbac import Resource from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.topic import TopicFollower from orm.user import User -from resolvers.reactions import reactions_follow, reactions_unfollow +from resolvers.zine.reactions import reactions_follow, reactions_unfollow from services.zine.gittask import GitTask diff --git a/resolvers/migrate.py b/resolvers/create/migrate.py similarity index 100% rename from resolvers/migrate.py rename to resolvers/create/migrate.py diff --git a/resolvers/inbox/chats.py b/resolvers/inbox/chats.py index 04fd691d..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 services.auth.users import UserStorage +from base.resolvers import mutation async def add_user_to_chat(user_slug: str, chat_id: str, chat=None): @@ -122,10 +121,3 @@ async def delete_chat(_, info, chat_id: str): return { "error": "chat not exist" } - - -@query.field("chatUsersAll") -@login_required -async def get_chat_users_all(_, info): - chat_users = await UserStorage.get_all_chat_users() - return chat_users diff --git a/resolvers/inbox/load.py b/resolvers/inbox/load.py index a3eb24fb..264e5c24 100644 --- a/resolvers/inbox/load.py +++ b/resolvers/inbox/load.py @@ -3,27 +3,11 @@ from datetime import datetime, timedelta from auth.authenticate import login_required from base.redis import redis +from base.orm import local_session from base.resolvers import query - - -async def get_unread_counter(chat_id: str, user_slug: str): - try: - unread = await redis.execute("LLEN", f"chats/{chat_id}/unread/{user_slug}") - if unread: - return unread - except Exception: - return 0 - - -async def get_total_unread_counter(user_slug: str): - chats = await redis.execute("GET", f"chats_by_user/{user_slug}") - unread = 0 - if chats: - chats = json.loads(chats) - for chat_id in chats: - n = await get_unread_counter(chat_id, user_slug) - unread += n - return unread +from orm.user import User +from resolvers.zine.profile import followed_authors +from .unread import get_unread_counter async def load_messages(chatId: str, limit: int, offset: int): @@ -100,3 +84,20 @@ async def load_messages_by(_, info, by, limit: int = 50, offset: int = 0): "messages": messages, "error": None } + + +@query.field("loadRecipients") +async def load_recipients(_, info, limit=50, offset=0): + chat_users = [] + user = info.context["request"].user + try: + chat_users += await followed_authors(user.slug) + limit = limit - len(chat_users) + except Exception: + pass + with local_session() as session: + chat_users += session.query(User).where(User.emailConfirmed).limit(limit).offset(offset) + return { + "members": chat_users, + "error": None + } diff --git a/resolvers/inbox/search.py b/resolvers/inbox/search.py index 5d6d4b82..914af4eb 100644 --- a/resolvers/inbox/search.py +++ b/resolvers/inbox/search.py @@ -7,9 +7,9 @@ from base.orm import local_session from orm.user import AuthorFollower -@query.field("searchUsers") +@query.field("searchRecipients") @login_required -async def search_users(_, info, query: str, limit: int = 50, offset: int = 0): +async def search_recipients(_, info, query: str, limit: int = 50, offset: int = 0): result = [] # TODO: maybe redis scan? user = info.context["request"].user @@ -38,6 +38,6 @@ async def search_users(_, info, query: str, limit: int = 50, offset: int = 0): result += session.query(AuthorFollower.follower).where(AuthorFollower.author.startswith(query))\ .offset(offset + len(result)).limit(offset + len(result) + limit) return { - "slugs": list(result), + "members": list(result), "error": None } diff --git a/resolvers/inbox/unread.py b/resolvers/inbox/unread.py new file mode 100644 index 00000000..0075a877 --- /dev/null +++ b/resolvers/inbox/unread.py @@ -0,0 +1,22 @@ +from base.redis import redis +import json + + +async def get_unread_counter(chat_id: str, user_slug: str): + try: + unread = await redis.execute("LLEN", f"chats/{chat_id}/unread/{user_slug}") + if unread: + return unread + except Exception: + return 0 + + +async def get_total_unread_counter(user_slug: str): + chats = await redis.execute("GET", f"chats_by_user/{user_slug}") + unread = 0 + if chats: + chats = json.loads(chats) + for chat_id in chats: + n = await get_unread_counter(chat_id, user_slug) + unread += n + return unread diff --git a/resolvers/zine/following.py b/resolvers/zine/following.py new file mode 100644 index 00000000..d747156c --- /dev/null +++ b/resolvers/zine/following.py @@ -0,0 +1,47 @@ +from auth.authenticate import login_required +from base.resolvers import mutation +# from resolvers.community import community_follow, community_unfollow +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 + + +@mutation.field("follow") +@login_required +async def follow(_, info, what, slug): + user = info.context["request"].user + try: + if what == "AUTHOR": + author_follow(user, slug) + elif what == "TOPIC": + topic_follow(user, slug) + elif what == "COMMUNITY": + # community_follow(user, slug) + pass + elif what == "REACTIONS": + reactions_follow(user, slug) + except Exception as e: + return {"error": str(e)} + + return {} + + +@mutation.field("unfollow") +@login_required +async def unfollow(_, info, what, slug): + user = info.context["request"].user + + try: + if what == "AUTHOR": + author_unfollow(user, slug) + elif what == "TOPIC": + topic_unfollow(user, slug) + elif what == "COMMUNITY": + # community_unfollow(user, slug) + pass + elif what == "REACTIONS": + reactions_unfollow(user, slug) + except Exception as e: + return {"error": str(e)} + + return {} diff --git a/resolvers/zine.py b/resolvers/zine/load.py similarity index 53% rename from resolvers/zine.py rename to resolvers/zine/load.py index 5ad71850..e45039c6 100644 --- a/resolvers/zine.py +++ b/resolvers/zine/load.py @@ -1,21 +1,65 @@ -#!/usr/bin/env python3.10 from datetime import datetime, timedelta import sqlalchemy as sa from sqlalchemy.orm import selectinload from sqlalchemy.sql.expression import desc, asc, select, case -from auth.authenticate import login_required from base.orm import local_session -from base.resolvers import mutation, query +from base.resolvers import query from orm.shout import Shout from orm.reaction import Reaction, ReactionKind -# 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.zine.shoutauthor import ShoutAuthorStorage from services.stat.reacted import ReactedStorage +def apply_filters(filters, q, user=None): + if filters.get("reacted") and user: + q.join(Reaction, Reaction.createdBy == user.slug) + if filters.get("visibility"): + q = q.filter(Shout.visibility == filters.get("visibility")) + if filters.get("layout"): + q = q.filter(Shout.layout == filters.get("layout")) + if filters.get("author"): + q = q.filter(Shout.authors.any(slug=filters.get("author"))) + if filters.get("topic"): + q = q.filter(Shout.topics.any(slug=filters.get("topic"))) + if filters.get("title"): + q = q.filter(Shout.title.ilike(f'%{filters.get("title")}%')) + if filters.get("body"): + q = q.filter(Shout.body.ilike(f'%{filters.get("body")}%s')) + if filters.get("days"): + before = datetime.now() - timedelta(days=int(filters.get("days")) or 30) + q = q.filter(Shout.createdAt > before) + return q + + +def extract_order(o, q): + if o: + q = q.add_columns(sa.func.count(Reaction.id).label(o)) + if o == 'comments': + q = q.join(Reaction, Shout.slug == Reaction.shout) + q = q.filter(Reaction.body.is_not(None)) + elif o == 'reacted': + q = q.join( + Reaction + ).add_columns( + sa.func.max(Reaction.createdAt).label(o) + ) + elif o == "rating": + q = q.join(Reaction).add_columns(sa.func.sum(case( + (Reaction.kind == ReactionKind.AGREE, 1), + (Reaction.kind == ReactionKind.DISAGREE, -1), + (Reaction.kind == ReactionKind.PROOF, 1), + (Reaction.kind == ReactionKind.DISPROOF, -1), + (Reaction.kind == ReactionKind.ACCEPT, 1), + (Reaction.kind == ReactionKind.REJECT, -1), + (Reaction.kind == ReactionKind.LIKE, 1), + (Reaction.kind == ReactionKind.DISLIKE, -1), + else_=0 + )).label(o)) + return o + else: + return 'createdAt' + + @query.field("loadShout") async def load_shout(_, info, slug): with local_session() as session: @@ -48,7 +92,7 @@ async def load_shouts_by(_, info, options): offset: 0 limit: 50 order_by: 'createdAt' - order_by_desc: tr + order_by_desc: true } :return: Shout[] @@ -61,53 +105,9 @@ async def load_shouts_by(_, info, options): ).where( Shout.deletedAt.is_(None) ) - - if options.get("filters"): - if options.get("filters").get("reacted"): - user = info.context["request"].user - q.join(Reaction, Reaction.createdBy == user.slug) - if options.get("filters").get("visibility"): - q = q.filter(Shout.visibility == options.get("filters").get("visibility")) - if options.get("filters").get("layout"): - q = q.filter(Shout.layout == options.get("filters").get("layout")) - if options.get("filters").get("author"): - q = q.filter(Shout.authors.any(slug=options.get("filters").get("author"))) - if options.get("filters").get("topic"): - q = q.filter(Shout.topics.any(slug=options.get("filters").get("topic"))) - if options.get("filters").get("title"): - q = q.filter(Shout.title.ilike(f'%{options.get("filters").get("title")}%')) - if options.get("filters").get("body"): - q = q.filter(Shout.body.ilike(f'%{options.get("filters").get("body")}%s')) - if options.get("filters").get("days"): - before = datetime.now() - timedelta(days=int(options.get("filter").get("days")) or 30) - q = q.filter(Shout.createdAt > before) - o = options.get("order_by") - if o: - q = q.add_columns(sa.func.count(Reaction.id).label(o)) - if o == 'comments': - q = q.join(Reaction, Shout.slug == Reaction.shout) - q = q.filter(Reaction.body.is_not(None)) - elif o == 'reacted': - q = q.join( - Reaction - ).add_columns( - sa.func.max(Reaction.createdAt).label(o) - ) - elif o == "rating": - q = q.join(Reaction).add_columns(sa.func.sum(case( - (Reaction.kind == ReactionKind.AGREE, 1), - (Reaction.kind == ReactionKind.DISAGREE, -1), - (Reaction.kind == ReactionKind.PROOF, 1), - (Reaction.kind == ReactionKind.DISPROOF, -1), - (Reaction.kind == ReactionKind.ACCEPT, 1), - (Reaction.kind == ReactionKind.REJECT, -1), - (Reaction.kind == ReactionKind.LIKE, 1), - (Reaction.kind == ReactionKind.DISLIKE, -1), - else_=0 - )).label(o)) - order_by = o - else: - order_by = 'createdAt' + user = info.context["request"].user + q = apply_filters(options.get("filters"), q, user) + order_by = extract_order(options.get("order_by")) query_order_by = desc(order_by) if options.get("order_by_desc") else asc(order_by) offset = options.get("offset", 0) limit = options.get("limit", 10) @@ -121,44 +121,3 @@ async def load_shouts_by(_, info, options): a.caption = await ShoutAuthorStorage.get_author_caption(s.slug, a.slug) return shouts - - -@mutation.field("follow") -@login_required -async def follow(_, info, what, slug): - user = info.context["request"].user - try: - if what == "AUTHOR": - author_follow(user, slug) - elif what == "TOPIC": - topic_follow(user, slug) - elif what == "COMMUNITY": - # community_follow(user, slug) - pass - elif what == "REACTIONS": - reactions_follow(user, slug) - except Exception as e: - return {"error": str(e)} - - return {} - - -@mutation.field("unfollow") -@login_required -async def unfollow(_, info, what, slug): - user = info.context["request"].user - - try: - if what == "AUTHOR": - author_unfollow(user, slug) - elif what == "TOPIC": - topic_unfollow(user, slug) - elif what == "COMMUNITY": - # community_unfollow(user, slug) - pass - elif what == "REACTIONS": - reactions_unfollow(user, slug) - except Exception as e: - return {"error": str(e)} - - return {} diff --git a/resolvers/profile.py b/resolvers/zine/profile.py similarity index 99% rename from resolvers/profile.py rename to resolvers/zine/profile.py index 62b50e72..911c6058 100644 --- a/resolvers/profile.py +++ b/resolvers/zine/profile.py @@ -14,7 +14,7 @@ from services.stat.topicstat import TopicStat from services.zine.shoutauthor import ShoutAuthor # from .community import followed_communities -from .inbox.load import get_total_unread_counter +from resolvers.inbox.unread import get_total_unread_counter from .topics import get_topic_stat diff --git a/resolvers/reactions.py b/resolvers/zine/reactions.py similarity index 100% rename from resolvers/reactions.py rename to resolvers/zine/reactions.py diff --git a/resolvers/topics.py b/resolvers/zine/topics.py similarity index 97% rename from resolvers/topics.py rename to resolvers/zine/topics.py index 399953a7..b5824fc0 100644 --- a/resolvers/topics.py +++ b/resolvers/zine/topics.py @@ -9,7 +9,7 @@ from orm.topic import Topic, TopicFollower 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): @@ -17,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) diff --git a/schema.graphql b/schema.graphql index 3b0cdb89..0a6385bb 100644 --- a/schema.graphql +++ b/schema.graphql @@ -24,9 +24,11 @@ type AuthResult { } type ChatMember { + id: Int! slug: String! name: String! userpic: String + lastSeen: DateTime invitedAt: DateTime invitedBy: String # user slug # TODO: add more @@ -261,8 +263,8 @@ type Query { # inbox loadChats( limit: Int, offset: Int): Result! # your chats loadMessagesBy(by: MessagesBy!, limit: Int, offset: Int): Result! - searchUsers(query: String!, limit: Int, offset: Int): Result! - chatUsersAll: [ChatUser]! + loadRecipients(limit: Int, offset: Int): Result! + searchRecipients(query: String!, limit: Int, offset: Int): Result! # auth isEmailUsed(email: String!): Boolean! @@ -369,14 +371,6 @@ type User { oid: String } -type ChatUser { - id: Int! - slug: String! - name: String! - userpic: String - lastSeen: DateTime -} - type Collab { authors: [String]! invites: [String] @@ -482,7 +476,7 @@ type TopicStat { shouts: Int! followers: Int! authors: Int! - viewed: Int + # viewed: Int reacted: Int! commented: Int rating: Int diff --git a/services/auth/users.py b/services/auth/users.py index 7d4d6c0b..dad50043 100644 --- a/services/auth/users.py +++ b/services/auth/users.py @@ -1,6 +1,5 @@ import asyncio from sqlalchemy.orm import selectinload -from base.orm import local_session from orm.user import User @@ -33,11 +32,6 @@ class UserStorage: aaa.sort(key=lambda user: user.createdAt) return aaa - @staticmethod - async def get_all_chat_users(): - with local_session() as session: - return session.query(User).where(User.emailConfirmed).all() - @staticmethod async def get_top_users(): self = UserStorage diff --git a/services/search.py b/services/search.py index 88af35ff..7cf53157 100644 --- a/services/search.py +++ b/services/search.py @@ -2,7 +2,7 @@ import asyncio import json from base.redis import redis from orm.shout import Shout -from resolvers.zine import load_shouts_by +from resolvers.zine.load import load_shouts_by class SearchService: