core/resolvers/author.py
2024-01-24 18:19:26 +03:00

312 lines
11 KiB
Python

import time
from typing import List
import logging
from sqlalchemy import and_, case, distinct, func, literal, select, cast, Integer
from sqlalchemy.orm import aliased
from orm.author import Author, AuthorFollower, AuthorRating
from orm.community import Community
from orm.reaction import Reaction, ReactionKind
from orm.shout import ShoutAuthor, ShoutTopic, Shout
from orm.topic import Topic
from resolvers.community import followed_communities
from resolvers.reaction import reacted_shouts_updates as followed_reactions
from resolvers.topic import followed_topics
from services.auth import login_required
from services.db import local_session
from services.schema import mutation, query
from services.unread import get_total_unread_counter
from services.viewed import ViewedStorage
logging.basicConfig()
logger = logging.getLogger("\t[resolvers.author]\t")
logger.setLevel(logging.DEBUG)
def add_author_stat_columns(q):
shout_author_aliased = aliased(ShoutAuthor)
q = q.outerjoin(shout_author_aliased, shout_author_aliased.author == Author.id).add_columns(
func.count(distinct(shout_author_aliased.shout)).label("shouts_stat")
)
followers_table = aliased(AuthorFollower)
q = q.outerjoin(followers_table, followers_table.author == Author.id).add_columns(
func.count(distinct(followers_table.follower)).label("followers_stat")
)
followings_table = aliased(AuthorFollower)
q = q.outerjoin(followings_table, followings_table.follower == Author.id).add_columns(
func.count(distinct(followers_table.author)).label("followings_stat")
)
q = q.group_by(Author.id)
return q
async def get_authors_from_query(q):
authors = []
with local_session() as session:
for [author, shouts_stat, followers_stat, followings_stat] in session.execute(q):
author.stat = {
"shouts": shouts_stat,
"followers": followers_stat,
"followings": followings_stat,
"viewed": await ViewedStorage.get_author(author.slug),
}
authors.append(author)
return authors
async def author_followings(author_id: int):
# NOTE: topics, authors, shout-reactions and communities slugs list
return {
"unread": await get_total_unread_counter(author_id),
"topics": [t.slug for t in await followed_topics(author_id)],
"authors": [a.slug for a in await followed_authors(author_id)],
"reactions": [s.slug for s in await followed_reactions(author_id)],
"communities": [c.slug for c in [followed_communities(author_id)] if isinstance(c, Community)],
}
@mutation.field("update_profile")
@login_required
async def update_profile(_, info, profile):
user_id = info.context["user_id"]
with local_session() as session:
author = session.query(Author).where(Author.user == user_id).first()
Author.update(author, profile)
session.add(author)
session.commit()
return {"error": None, "author": author}
# for mutation.field("follow")
def author_follow(follower_id, slug):
try:
with local_session() as session:
author = session.query(Author).where(Author.slug == slug).one()
af = AuthorFollower(follower=follower_id, author=author.id)
session.add(af)
session.commit()
return True
except Exception:
return False
# for mutation.field("unfollow")
def author_unfollow(follower_id, slug):
with local_session() as session:
flw = (
session.query(AuthorFollower)
.join(Author, Author.id == AuthorFollower.author)
.filter(and_(AuthorFollower.follower == follower_id, Author.slug == slug))
.first()
)
if flw:
session.delete(flw)
session.commit()
return True
return False
# TODO: caching query
@query.field("get_authors_all")
async def get_authors_all(_, _info):
with local_session() as session:
return session.query(Author).all()
def count_author_comments_rating(session, author_id) -> int:
replied_alias = aliased(Reaction)
replies_likes = (
session.query(replied_alias)
.join(Reaction, replied_alias.id == Reaction.reply_to)
.where(and_(replied_alias.created_by == author_id, replied_alias.kind == ReactionKind.COMMENT.value))
.filter(replied_alias.kind == ReactionKind.LIKE.value)
.count()
) or 0
replies_dislikes = (
session.query(replied_alias)
.join(Reaction, replied_alias.id == Reaction.reply_to)
.where(and_(replied_alias.created_by == author_id, replied_alias.kind == ReactionKind.COMMENT.value))
.filter(replied_alias.kind == ReactionKind.DISLIKE.value)
.count()
) or 0
return replies_likes - replies_dislikes
def count_author_shouts_rating(session, author_id) -> int:
shouts_likes = (
session.query(Reaction, Shout)
.join(Shout, Shout.id == Reaction.shout)
.filter(and_(Shout.authors.any(id=author_id), Reaction.kind == ReactionKind.LIKE.value))
.count()
or 0
)
shouts_dislikes = (
session.query(Reaction, Shout)
.join(Shout, Shout.id == Reaction.shout)
.filter(and_(Shout.authors.any(id=author_id), Reaction.kind == ReactionKind.DISLIKE.value))
.count()
or 0
)
return shouts_likes - shouts_dislikes
async def load_author_with_stats(q):
q = add_author_stat_columns(q)
result = await get_authors_from_query(q)
if result:
[author] = result
with local_session() as session:
comments_count = (
session.query(Reaction)
.filter(
and_(
Reaction.created_by == author.id,
Reaction.kind == ReactionKind.COMMENT.value,
Reaction.deleted_at.is_(None),
)
)
.count()
)
ratings_sum = (
session.query(
func.sum(
case((AuthorRating.plus == True, cast(1, Integer)),
else_=cast(-1, Integer))).label("rating")
)
.filter(AuthorRating.author == author.id)
.scalar()
)
author.stat["rating"] = ratings_sum or 0
author.stat["rating_shouts"] = count_author_shouts_rating(session, author.id)
author.stat["rating_comments"] = count_author_comments_rating(session, author.id)
author.stat["commented"] = comments_count
return author
@query.field("get_author")
async def get_author(_, _info, slug="", author_id=None):
q = None
if slug or author_id:
if bool(slug):
q = select(Author).where(Author.slug == slug)
if author_id:
q = select(Author).where(Author.id == author_id)
return await load_author_with_stats(q)
@query.field("get_author_id")
async def get_author_id(_, _info, user: str):
with local_session() as session:
logger.info(f"getting author id for {user}")
q = select(Author).filter(Author.user == user)
return await load_author_with_stats(q)
@query.field("load_authors_by")
async def load_authors_by(_, _info, by, limit, offset):
q = select(Author)
q = add_author_stat_columns(q)
if by.get("slug"):
q = q.filter(Author.slug.ilike(f"%{by['slug']}%"))
elif by.get("name"):
q = q.filter(Author.name.ilike(f"%{by['name']}%"))
elif by.get("topic"):
q = q.join(ShoutAuthor).join(ShoutTopic).join(Topic).where(Topic.slug == by["topic"])
if by.get("last_seen"): # in unixtime
before = int(time.time()) - by["last_seen"]
q = q.filter(Author.last_seen > before)
elif by.get("created_at"): # in unixtime
before = int(time.time()) - by["created_at"]
q = q.filter(Author.created_at > before)
q = q.order_by(by.get("order", Author.created_at)).limit(limit).offset(offset)
return await get_authors_from_query(q)
@query.field("get_author_followed")
async def get_author_followed(_, _info, slug="", user=None, author_id=None) -> List[Author]:
author_id_query = None
if slug:
author_id_query = select(Author.id).where(Author.slug == slug)
elif user:
author_id_query = select(Author.id).where(Author.user == user)
if author_id_query is not None and not author_id:
with local_session() as session:
author_id = session.execute(author_id_query).scalar()
if author_id is None:
raise ValueError("Author not found")
else:
return await followed_authors(author_id) # Author[]
@query.field("get_author_followers")
async def get_author_followers(_, _info, slug) -> List[Author]:
q = select(Author)
q = add_author_stat_columns(q)
aliased_author = aliased(Author)
q = (
q.join(AuthorFollower, AuthorFollower.follower == Author.id)
.join(aliased_author, aliased_author.id == AuthorFollower.author)
.where(aliased_author.slug == slug)
)
return await get_authors_from_query(q)
async def followed_authors(follower_id):
q = select(Author)
q = add_author_stat_columns(q)
q = q.join(AuthorFollower, AuthorFollower.author == Author.id).where(AuthorFollower.follower == follower_id)
# Pass the query to the get_authors_from_query function and return the results
return await get_authors_from_query(q)
@mutation.field("rate_author")
@login_required
async def rate_author(_, info, rated_slug, value):
user_id = info.context["user_id"]
with local_session() as session:
rated_author = session.query(Author).filter(Author.slug == rated_slug).first()
rater = session.query(Author).filter(Author.slug == user_id).first()
if rater and rated_author:
rating: AuthorRating = (
session.query(AuthorRating)
.filter(and_(AuthorRating.rater == rater.id, AuthorRating.author == rated_author.id))
.first()
)
if rating:
rating.plus = value > 0
session.add(rating)
session.commit()
return {}
else:
try:
rating = AuthorRating(rater=rater.id, author=rated_author.id, plus=value > 0)
session.add(rating)
session.commit()
except Exception as err:
return {"error": err}
return {}
async def create_author(user_id: str, slug: str, name: str = ""):
with local_session() as session:
new_author = Author(user=user_id, slug=slug, name=name)
session.add(new_author)
session.commit()
logger.info(f"author created by webhook {new_author.dict()}")