merged-isolated-core
Some checks failed
deploy / deploy (push) Failing after 1m46s

This commit is contained in:
2023-10-23 17:47:11 +03:00
parent b675188013
commit bf241a8fbd
56 changed files with 1683 additions and 2784 deletions

View File

@@ -1,36 +1,12 @@
from resolvers.auth import (
login,
sign_out,
is_email_used,
register_by_email,
confirm_email,
auth_send_link,
get_current_user,
)
from resolvers.editor import create_shout, delete_shout, update_shout
from resolvers.profile import (
from resolvers.author import (
load_authors_by,
rate_user,
update_profile,
get_authors_all,
author_followers,
author_followings,
get_followed_authors,
get_author,
get_author_by_id
)
from resolvers.topics import (
topics_all,
topics_by_community,
topics_by_author,
topic_follow,
topic_unfollow,
get_topic,
)
from resolvers.reactions import (
from resolvers.reaction import (
create_reaction,
delete_reaction,
update_reaction,
@@ -38,52 +14,50 @@ from resolvers.reactions import (
reactions_follow,
load_reactions_by,
)
from resolvers.topic import (
topic_follow,
topic_unfollow,
topics_by_author,
topics_by_community,
topics_all,
get_topic,
)
from resolvers.following import follow, unfollow
from resolvers.load import load_shout, load_shouts_by
from resolvers.follower import follow, unfollow
from resolvers.reader import load_shout, load_shouts_by
from resolvers.community import get_community, get_communities_all
__all__ = [
# auth
"login",
"register_by_email",
"is_email_used",
"confirm_email",
"auth_send_link",
"sign_out",
"get_current_user",
# profile
# author
"load_authors_by",
"rate_user",
"update_profile",
"get_authors_all",
"author_followers",
"author_followings",
"get_followed_authors",
"get_author",
"get_author_by_id",
# load
# reader
"load_shout",
"load_shouts_by",
# zine.following
"rate_author",
# follower
"follow",
"unfollow",
# create
# editor
"create_shout",
"update_shout",
"delete_shout",
# topics
# topic
"topics_all",
"topics_by_community",
"topics_by_author",
"topic_follow",
"topic_unfollow",
"get_topic",
# zine.reactions
# reaction
"reactions_follow",
"reactions_unfollow",
"create_reaction",
"update_reaction",
"delete_reaction",
"load_reactions_by",
# community
"get_community",
"get_communities_all",
]

View File

@@ -1,211 +0,0 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timezone
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
from auth.identity import Identity, Password
from auth.jwtcodec import JWTCodec
from auth.tokenstorage import TokenStorage
from services.exceptions import (
BaseHttpException,
InvalidPassword,
InvalidToken,
ObjectNotExist,
Unauthorized,
)
from services.db import local_session
from services.schema import mutation, query
from orm import Role, User
from resolvers.profile import user_subscriptions
from settings import SESSION_TOKEN_HEADER, FRONTEND_URL
@mutation.field("getSession")
@login_required
async def get_current_user(_, info):
auth: AuthCredentials = info.context["request"].auth
token = info.context["request"].headers.get(SESSION_TOKEN_HEADER)
token = token.split(" ")[-1]
with local_session() as session:
user = session.query(User).where(User.id == auth.user_id).one()
user.lastSeen = datetime.now(tz=timezone.utc)
session.commit()
return {
"token": token,
"user": user,
"news": await user_subscriptions(user.id),
}
@mutation.field("confirmEmail")
async def confirm_email(_, info, token):
"""confirm owning email address"""
try:
print("[resolvers.auth] confirm email by token")
payload = JWTCodec.decode(token)
user_id = payload.user_id
await TokenStorage.get(f"{user_id}-{payload.username}-{token}")
with local_session() as session:
user = session.query(User).where(User.id == user_id).first()
session_token = await TokenStorage.create_session(user)
user.emailConfirmed = True
user.lastSeen = datetime.now(tz=timezone.utc)
session.add(user)
session.commit()
return {
"token": session_token,
"user": user,
"news": await user_subscriptions(user.id),
}
except InvalidToken as e:
raise InvalidToken(e.message)
except Exception as e:
print(e) # FIXME: debug only
return {"error": "email is not confirmed"}
async def confirm_email_handler(request):
token = request.path_params["token"] # one time
request.session["token"] = token
res = await confirm_email(None, {}, token)
print("[resolvers.auth] confirm_email request: %r" % request)
if "error" in res:
raise BaseHttpException(res["error"])
else:
response = RedirectResponse(url=FRONTEND_URL)
response.set_cookie("token", res["token"]) # session token
return response
def create_user(user_dict):
user = User(**user_dict)
with local_session() as session:
user.roles.append(session.query(Role).first())
session.add(user)
session.commit()
return user
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
with local_session() as session:
user = session.query(User).where(User.slug == slug).first()
while user:
user = session.query(User).where(User.slug == slug).first()
slug = slug + "-" + str(c)
c += 1
if not user:
unique_slug = slug
print("[resolvers.auth] " + unique_slug)
return quote_plus(unique_slug.replace("'", "")).replace("+", "-")
@mutation.field("registerUser")
async def register_by_email(_, _info, email: str, password: str = "", name: str = ""):
email = email.lower()
"""creates new user account"""
with local_session() as session:
user = session.query(User).filter(User.email == email).first()
if user:
raise Unauthorized("User already exist")
else:
slug = generate_unique_slug(name)
user = session.query(User).where(User.slug == slug).first()
if user:
slug = generate_unique_slug(email.split("@")[0])
user_dict = {
"email": email,
"username": email, # will be used to store phone number or some messenger network id
"name": name,
"slug": slug,
}
if password:
user_dict["password"] = Password.encode(password)
user = create_user(user_dict)
user = await auth_send_link(_, _info, email)
return {"user": user}
@mutation.field("sendLink")
async def auth_send_link(_, _info, email, lang="ru", template="email_confirmation"):
email = email.lower()
"""send link with confirm code to email"""
with local_session() as session:
user = session.query(User).filter(User.email == email).first()
if not user:
raise ObjectNotExist("User not found")
else:
token = await TokenStorage.create_onetime(user)
await send_auth_email(user, token, lang, template)
return user
@query.field("signIn")
async def login(_, info, email: str, password: str = "", lang: str = "ru"):
email = email.lower()
with local_session() as session:
orm_user = session.query(User).filter(User.email == email).first()
if orm_user is None:
print(f"[auth] {email}: email not found")
# return {"error": "email not found"}
raise ObjectNotExist("User not found") # contains webserver status
if not password:
print(f"[auth] send confirm link to {email}")
token = await TokenStorage.create_onetime(orm_user)
await send_auth_email(orm_user, token, lang)
# FIXME: not an error, warning
return {"error": "no password, email link was sent"}
else:
# sign in using password
if not orm_user.emailConfirmed:
# not an error, warns users
return {"error": "please, confirm email"}
else:
try:
user = Identity.password(orm_user, password)
session_token = await TokenStorage.create_session(user)
print(f"[auth] user {email} authorized")
return {
"token": session_token,
"user": user,
"news": await user_subscriptions(user.id),
}
except InvalidPassword:
print(f"[auth] {email}: invalid password")
raise InvalidPassword(
"invalid password"
) # contains webserver status
# return {"error": "invalid password"}
@query.field("signOut")
@login_required
async def sign_out(_, info: GraphQLResolveInfo):
token = info.context["request"].headers.get(SESSION_TOKEN_HEADER, "")
status = await TokenStorage.revoke(token)
return status
@query.field("isEmailUsed")
async def is_email_used(_, _info, email):
email = email.lower()
with local_session() as session:
user = session.query(User).filter(User.email == email).first()
return user is not None

242
resolvers/author.py Normal file
View File

@@ -0,0 +1,242 @@
from typing import List
from datetime import datetime, timedelta, timezone
from sqlalchemy import and_, func, distinct, select, literal
from sqlalchemy.orm import aliased
from services.auth import login_required
from base.orm import local_session
from base.resolvers import mutation, query
from orm.shout import ShoutAuthor, ShoutTopic
from orm.topic import Topic
from orm.author import AuthorFollower, Author, AuthorRating
from community import followed_communities
from topic import followed_topics
from reaction import load_followed_reactions
def add_author_stat_columns(q):
followers_table = aliased(AuthorFollower)
followings_table = aliased(AuthorFollower)
shout_author_aliased = aliased(ShoutAuthor)
# author_rating_aliased = aliased(AuthorRating)
q = q.outerjoin(shout_author_aliased).add_columns(
func.count(distinct(shout_author_aliased.shout)).label("shouts_stat")
)
q = q.outerjoin(followers_table, followers_table.author == Author.id).add_columns(
func.count(distinct(followers_table.follower)).label("followers_stat")
)
q = q.outerjoin(
followings_table, followings_table.follower == Author.id
).add_columns(
func.count(distinct(followings_table.author)).label("followings_stat")
)
q = q.add_columns(literal(0).label("rating_stat"))
# FIXME
# q = q.outerjoin(author_rating_aliased, author_rating_aliased.user == Author.id).add_columns(
# # TODO: check
# func.sum(author_rating_aliased.value).label('rating_stat')
# )
q = q.add_columns(literal(0).label("commented_stat"))
# q = q.outerjoin(Reaction, and_(Reaction.createdBy == Author.id, Reaction.body.is_not(None))).add_columns(
# func.count(distinct(Reaction.id)).label('commented_stat')
# )
q = q.group_by(Author.id)
return q
def add_stat(author, stat_columns):
[
shouts_stat,
followers_stat,
followings_stat,
rating_stat,
commented_stat,
] = stat_columns
author.stat = {
"shouts": shouts_stat,
"followers": followers_stat,
"followings": followings_stat,
"rating": rating_stat,
"commented": commented_stat,
}
return author
def get_authors_from_query(q):
authors = []
with local_session() as session:
for [author, *stat_columns] in session.execute(q):
author = add_stat(author, stat_columns)
authors.append(author)
return authors
async def author_followings(author_id: int):
return {
# "unread": await get_total_unread_counter(author_id), # unread inbox messages counter
"topics": [
t.slug for t in await followed_topics(author_id)
], # followed topics slugs
"authors": [
a.slug for a in await followed_authors(author_id)
], # followed authors slugs
"reactions": await load_followed_reactions(author_id),
"communities": [
c.slug for c in await followed_communities(author_id)
], # communities
}
@mutation.field("updateProfile")
@login_required
async def update_profile(_, info, profile):
author_id = info.context["author_id"]
with local_session() as session:
author = session.query(Author).where(Author.id == author_id).first()
author.update(profile)
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.create(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
@query.field("authorsAll")
async def get_authors_all(_, _info):
q = select(Author)
q = add_author_stat_columns(q)
q = q.join(ShoutAuthor, Author.id == ShoutAuthor.author)
return get_authors_from_query(q)
@query.field("getAuthor")
async def get_author(_, _info, slug):
q = select(Author).where(Author.slug == slug)
q = add_author_stat_columns(q)
authors = get_authors_from_query(q)
return authors[0]
@query.field("loadAuthorsBy")
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("lastSeen"): # in days
days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["lastSeen"])
q = q.filter(Author.lastSeen > days_before)
elif by.get("createdAt"): # in days
days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["createdAt"])
q = q.filter(Author.createdAt > days_before)
q = q.order_by(by.get("order", Author.createdAt)).limit(limit).offset(offset)
return get_authors_from_query(q)
async def get_followed_authors(_, _info, slug) -> List[Author]:
# First, we need to get the author_id for the given slug
with local_session() as session:
author_id_query = select(Author.id).where(Author.slug == slug)
author_id = session.execute(author_id_query).scalar()
if author_id is None:
raise ValueError("Author not found")
return await followed_authors(author_id)
async def 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 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 get_authors_from_query(q)
@mutation.field("rateAuthor")
@login_required
async def rate_author(_, info, rated_user_id, value):
author_id = info.context["author_id"]
with local_session() as session:
rating = (
session.query(AuthorRating)
.filter(
and_(
AuthorRating.rater == author_id, AuthorRating.user == rated_user_id
)
)
.first()
)
if rating:
rating.value = value
session.commit()
return {}
try:
AuthorRating.create(rater=author_id, user=rated_user_id, value=value)
except Exception as err:
return {"error": err}
return {}

View File

@@ -1,8 +1,116 @@
from base.orm import local_session
from base.resolvers import query
from orm.author import Author
from orm.community import Community, CommunityAuthor
from orm.shout import ShoutCommunity
from sqlalchemy import select, distinct, func, literal, and_
from sqlalchemy.orm import aliased
def add_community_stat_columns(q):
community_followers = aliased(CommunityAuthor)
shout_community_aliased = aliased(ShoutCommunity)
q = q.outerjoin(shout_community_aliased).add_columns(
func.count(distinct(shout_community_aliased.shout)).label("shouts_stat")
)
q = q.outerjoin(
community_followers, community_followers.author == Author.id
).add_columns(
func.count(distinct(community_followers.follower)).label("followers_stat")
)
q = q.add_columns(literal(0).label("rating_stat"))
# FIXME
# q = q.outerjoin(author_rating_aliased, author_rating_aliased.user == Author.id).add_columns(
# # TODO: check
# func.sum(author_rating_aliased.value).label('rating_stat')
# )
q = q.add_columns(literal(0).label("commented_stat"))
# q = q.outerjoin(Reaction, and_(Reaction.createdBy == Author.id, Reaction.body.is_not(None))).add_columns(
# func.count(distinct(Reaction.id)).label('commented_stat')
# )
q = q.group_by(Author.id)
return q
def get_communities_from_query(q):
ccc = []
with local_session() as session:
for [c, *stat_columns] in session.execute(q):
[shouts_stat, followers_stat, rating_stat, commented_stat] = stat_columns
c.stat = {
"shouts": shouts_stat,
"followers": followers_stat,
"rating": rating_stat,
"commented": commented_stat,
}
ccc.append(c)
return ccc
def followed_communities(follower_id):
amount = select(Community).count()
if amount < 2:
# no need to run long query most of the cases
return [
select(Community).first(),
]
else:
q = select(Community)
q = add_community_stat_columns(q)
q = q.join(CommunityAuthor, CommunityAuthor.community == Community.id).where(
CommunityAuthor.follower == follower_id
)
# 3. Pass the query to the get_authors_from_query function and return the results
return get_communities_from_query(q)
# for mutation.field("follow")
def community_follow(follower_id, slug):
# TODO: implement when needed
return None
try:
with local_session() as session:
community = session.query(Community).where(Community.slug == slug).one()
cf = CommunityAuthor.create(author=follower_id, community=community.id)
session.add(cf)
session.commit()
return True
except Exception:
return False
# for mutation.field("unfollow")
def community_unfollow(follower_id, slug):
# TODO: implement
return None
with local_session() as session:
flw = (
session.query(CommunityAuthor)
.join(Community, Community.id == CommunityAuthor.community)
.filter(and_(CommunityAuthor.author == follower_id, Community.slug == slug))
.first()
)
if flw:
session.delete(flw)
session.commit()
return True
return False
@query.field("communitiesAll")
async def get_communities_all(_, _info):
q = select(Author)
q = add_community_stat_columns(q)
return get_communities_from_query(q)
@query.field("getCommunity")
async def get_community(_, _info, slug):
q = select(Community).where(Community.slug == slug)
q = add_community_stat_columns(q)
authors = get_communities_from_query(q)
return authors[0]

View File

@@ -1,23 +1,44 @@
from datetime import datetime, timezone
from sqlalchemy import and_
from sqlalchemy import and_, select
from sqlalchemy.orm import joinedload
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from services.db import local_session
from services.schema import mutation
from services.auth import login_required
from base.orm import local_session
from base.resolvers import mutation, query
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import Topic
from resolvers.reactions import reactions_follow, reactions_unfollow
from reaction import reactions_follow, reactions_unfollow
from services.presence import notify_shout
@query.field("loadDrafts")
async def get_drafts(_, info):
author = info.context["request"].author
q = (
select(Shout)
.options(
joinedload(Shout.authors),
joinedload(Shout.topics),
)
.where(and_(Shout.deletedAt.is_(None), Shout.createdBy == author.id))
)
q = q.group_by(Shout.id)
shouts = []
with local_session() as session:
for [shout] in session.execute(q).unique():
shouts.append(shout)
return shouts
@mutation.field("createShout")
@login_required
async def create_shout(_, info, inp):
auth: AuthCredentials = info.context["request"].auth
author_id = info.context["author_id"]
with local_session() as session:
topics = (
session.query(Topic).filter(Topic.slug.in_(inp.get("topics", []))).all()
@@ -34,8 +55,8 @@ async def create_shout(_, info, inp):
"authors": inp.get("authors", []),
"slug": inp.get("slug"),
"mainTopic": inp.get("mainTopic"),
"visibility": "owner",
"createdBy": auth.user_id,
"visibility": "authors",
"createdBy": author_id,
}
)
@@ -44,12 +65,12 @@ async def create_shout(_, info, inp):
session.add(t)
# NOTE: shout made by one first author
sa = ShoutAuthor.create(shout=new_shout.id, user=auth.user_id)
sa = ShoutAuthor.create(shout=new_shout.id, author=author_id)
session.add(sa)
session.add(new_shout)
reactions_follow(auth.user_id, new_shout.id, True)
reactions_follow(author_id, new_shout.id, True)
session.commit()
@@ -59,6 +80,8 @@ async def create_shout(_, info, inp):
if new_shout.slug is None:
new_shout.slug = f"draft-{new_shout.id}"
session.commit()
else:
notify_shout(new_shout.dict(), "create")
return {"shout": new_shout}
@@ -66,7 +89,7 @@ async def create_shout(_, info, inp):
@mutation.field("updateShout")
@login_required
async def update_shout(_, info, shout_id, shout_input=None, publish=False):
auth: AuthCredentials = info.context["request"].auth
author_id = info.context["author_id"]
with local_session() as session:
shout = (
@@ -82,7 +105,7 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False):
if not shout:
return {"error": "shout not found"}
if shout.createdBy != auth.user_id:
if shout.createdBy != author_id:
return {"error": "access denied"}
updated = False
@@ -154,33 +177,39 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False):
shout.update(shout_input)
updated = True
if publish and shout.visibility == "owner":
# TODO: use visibility setting
if publish and shout.visibility == "authors":
shout.visibility = "community"
shout.publishedAt = datetime.now(tz=timezone.utc)
updated = True
# notify on publish
notify_shout(shout.dict())
if updated:
shout.updatedAt = datetime.now(tz=timezone.utc)
session.commit()
# GitTask(inp, user.username, user.email, "update shout %s" % slug)
notify_shout(shout.dict(), "update")
return {"shout": shout}
@mutation.field("deleteShout")
@login_required
async def delete_shout(_, info, shout_id):
auth: AuthCredentials = info.context["request"].auth
author_id = info.context["author_id"]
with local_session() as session:
shout = session.query(Shout).filter(Shout.id == shout_id).first()
if not shout:
return {"error": "invalid shout id"}
if auth.user_id != shout.createdBy:
if author_id != shout.createdBy:
return {"error": "access denied"}
for author_id in shout.authors:
@@ -189,4 +218,7 @@ async def delete_shout(_, info, shout_id):
shout.deletedAt = datetime.now(tz=timezone.utc)
session.commit()
notify_shout(shout.dict(), "delete")
return {}

71
resolvers/follower.py Normal file
View File

@@ -0,0 +1,71 @@
from services.auth import login_required
from resolvers.author import author_follow, author_unfollow
from resolvers.reaction import reactions_follow, reactions_unfollow
from resolvers.topic import topic_follow, topic_unfollow
from resolvers.community import community_follow, community_unfollow
from services.following import FollowingManager, FollowingResult
from services.db import local_session
from orm.author import Author
from services.presence import notify_follower
@login_required
async def follow(_, info, what, slug):
follower_id = info.context["author_id"]
try:
if what == "AUTHOR":
if author_follow(follower_id, slug):
result = FollowingResult("NEW", 'author', slug)
await FollowingManager.push('author', result)
with local_session() as session:
author = session.query(Author.id).where(Author.slug == slug).one()
follower = session.query(Author).where(Author.id == follower_id).one()
notify_follower(follower.dict(), author.id)
elif what == "TOPIC":
if topic_follow(follower_id, slug):
result = FollowingResult("NEW", 'topic', slug)
await FollowingManager.push('topic', result)
elif what == "COMMUNITY":
if community_follow(follower_id, slug):
result = FollowingResult("NEW", 'community', slug)
await FollowingManager.push('community', result)
elif what == "REACTIONS":
if reactions_follow(follower_id, slug):
result = FollowingResult("NEW", 'shout', slug)
await FollowingManager.push('shout', result)
except Exception as e:
print(Exception(e))
return {"error": str(e)}
return {}
@login_required
async def unfollow(_, info, what, slug):
follower_id = info.context["author_id"]
try:
if what == "AUTHOR":
if author_unfollow(follower_id, slug):
result = FollowingResult("DELETED", 'author', slug)
await FollowingManager.push('author', result)
with local_session() as session:
author = session.query(Author.id).where(Author.slug == slug).one()
follower = session.query(Author).where(Author.id == follower_id).one()
notify_follower(follower.dict(), author.id)
elif what == "TOPIC":
if topic_unfollow(follower_id, slug):
result = FollowingResult("DELETED", 'topic', slug)
await FollowingManager.push('topic', result)
elif what == "COMMUNITY":
if community_unfollow(follower_id, slug):
result = FollowingResult("DELETED", 'community', slug)
await FollowingManager.push('community', result)
elif what == "REACTIONS":
if reactions_unfollow(follower_id, slug):
result = FollowingResult("DELETED", 'shout', slug)
await FollowingManager.push('shout', result)
except Exception as e:
return {"error": str(e)}
return {}

View File

@@ -1,72 +0,0 @@
from services.schema import mutation
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
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.following import FollowingManager, FollowingResult
from resolvers.community import community_follow, community_unfollow
from services.presence import notify_follower
from orm.user import User
from services.db import local_session
@mutation.field("follow")
@login_required
async def follow(_, info, what, slug):
auth: AuthCredentials = info.context["request"].auth
try:
if what == "AUTHOR":
if author_follow(auth.user_id, slug):
result = FollowingResult("NEW", "author", slug)
await FollowingManager.push("author", result)
with local_session() as session:
author = session.query(User.id).where(User.slug == slug).one()
follower = session.query(User.id).where(User.id == auth.user_id).one()
notify_follower(follower.dict(), author.id)
elif what == "TOPIC":
if topic_follow(auth.user_id, slug):
result = FollowingResult("NEW", "topic", slug)
await FollowingManager.push("topic", result)
elif what == "COMMUNITY":
if community_follow(auth.user_id, slug):
result = FollowingResult("NEW", "community", slug)
await FollowingManager.push("community", result)
elif what == "REACTIONS":
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 {}
@mutation.field("unfollow")
@login_required
async def unfollow(_, info, what, slug):
auth: AuthCredentials = info.context["request"].auth
try:
if what == "AUTHOR":
if author_unfollow(auth.user_id, slug):
result = FollowingResult("DELETED", "author", slug)
await FollowingManager.push("author", result)
elif what == "TOPIC":
if topic_unfollow(auth.user_id, slug):
result = FollowingResult("DELETED", "topic", slug)
await FollowingManager.push("topic", result)
elif what == "COMMUNITY":
if community_unfollow(auth.user_id, slug):
result = FollowingResult("DELETED", "community", slug)
await FollowingManager.push("community", result)
elif what == "REACTIONS":
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)}
return {}

View File

@@ -1,348 +0,0 @@
from typing import List
from datetime import datetime, timedelta, timezone
from sqlalchemy import and_, func, distinct, select, literal
from sqlalchemy.orm import aliased, joinedload
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from services.db import local_session
from services.schema import mutation, query
from orm.reaction import Reaction, ReactionKind
from orm.shout import ShoutAuthor, ShoutTopic
from orm.topic import Topic
from orm.user import AuthorFollower, Role, User, UserRating, UserRole
# from .community import followed_communities
from services.unread import get_total_unread_counter
from resolvers.topics import followed_by_user as followed_topics
def add_author_stat_columns(q, full=False):
author_followers = aliased(AuthorFollower)
author_following = aliased(AuthorFollower)
shout_author_aliased = aliased(ShoutAuthor)
q = q.outerjoin(shout_author_aliased).add_columns(
func.count(distinct(shout_author_aliased.shout)).label("shouts_stat")
)
q = q.outerjoin(author_followers, author_followers.author == User.id).add_columns(
func.count(distinct(author_followers.follower)).label("followers_stat")
)
q = q.outerjoin(author_following, author_following.follower == User.id).add_columns(
func.count(distinct(author_following.author)).label("followings_stat")
)
if full:
user_rating_aliased = aliased(UserRating)
q = q.outerjoin(
user_rating_aliased, user_rating_aliased.user == User.id
).add_columns(func.sum(user_rating_aliased.value).label("rating_stat"))
else:
q = q.add_columns(literal(-1).label("rating_stat"))
if full:
q = q.outerjoin(
Reaction, and_(Reaction.createdBy == User.id, Reaction.body.is_not(None))
).add_columns(func.count(distinct(Reaction.id)).label("commented_stat"))
else:
q = q.add_columns(literal(-1).label("commented_stat"))
q = q.group_by(User.id)
return q
def add_stat(author, stat_columns):
[
shouts_stat,
followers_stat,
followings_stat,
rating_stat,
commented_stat,
] = stat_columns
author.stat = {
"shouts": shouts_stat,
"followers": followers_stat,
"followings": followings_stat,
"rating": rating_stat,
"commented": commented_stat,
}
return author
def get_authors_from_query(q):
authors = []
with local_session() as session:
for [author, *stat_columns] in session.execute(q):
author = add_stat(author, stat_columns)
authors.append(author)
return authors
async def user_subscriptions(user_id: int):
return {
"unread": await get_total_unread_counter(
user_id
), # unread inbox messages counter
"topics": [
t.slug for t in followed_topics(user_id)
], # followed topics slugs
"authors": [
a.slug for a in followed_authors(user_id)
], # followed authors slugs
"reactions": await followed_reactions(user_id)
# "communities": [c.slug for c in followed_communities(slug)], # communities
}
# @query.field("userFollowedDiscussions")
# @login_required
async def followed_discussions(_, info, user_id) -> List[Topic]:
return await followed_reactions(user_id)
async def followed_reactions(user_id):
with local_session() as session:
user = session.query(User).where(User.id == user_id).first()
return (
session.query(Reaction.shout)
.where(Reaction.createdBy == user.id)
.filter(Reaction.createdAt > user.lastSeen)
.all()
)
# dufok mod (^*^') :
@query.field("userFollowedTopics")
async def get_followed_topics(_, info, slug) -> List[Topic]:
user_id_query = select(User.id).where(User.slug == slug)
with local_session() as session:
user_id = session.execute(user_id_query).scalar()
if user_id is None:
raise ValueError("User not found")
return followed_topics(user_id)
# dufok mod (^*^') :
@query.field("userFollowedAuthors")
async def get_followed_authors(_, _info, slug) -> List[User]:
# 1. First, we need to get the user_id for the given slug
user_id_query = select(User.id).where(User.slug == slug)
with local_session() as session:
user_id = session.execute(user_id_query).scalar()
if user_id is None:
raise ValueError("User not found")
return followed_authors(user_id)
@query.field("authorFollowings")
async def author_followings(_, info, author_id: int, limit: int = 20, offset: int = 0) -> List[User]:
return followed_authors(author_id)[offset:(limit+offset)]
@query.field("authorFollowers")
async def author_followers(_, info, author_id: int, limit: int = 20, offset: int = 0) -> List[User]:
q = select(User)
q = add_author_stat_columns(q)
aliased_user = aliased(User)
q = (
q.join(AuthorFollower, AuthorFollower.follower == User.id)
.join(aliased_user, aliased_user.id == AuthorFollower.author)
.where(aliased_user.id == author_id)
.limit(limit)
.offset(offset)
)
return get_authors_from_query(q)
# 2. Now, we can use the user_id to get the followed authors
def followed_authors(user_id):
q = select(User)
q = add_author_stat_columns(q)
q = q.join(AuthorFollower, AuthorFollower.author == User.id).where(
AuthorFollower.follower == user_id
)
# 3. Pass the query to the get_authors_from_query function and return the results
return get_authors_from_query(q)
@query.field("userFollowers")
async def user_followers(_, _info, slug) -> List[User]:
q = select(User)
q = add_author_stat_columns(q)
aliased_user = aliased(User)
q = (
q.join(AuthorFollower, AuthorFollower.follower == User.id)
.join(aliased_user, aliased_user.id == AuthorFollower.author)
.where(aliased_user.slug == slug)
)
return get_authors_from_query(q)
async def get_user_roles(slug):
with local_session() as session:
user = session.query(User).where(User.slug == slug).first()
roles = (
session.query(Role)
.options(joinedload(Role.permissions))
.join(UserRole)
.where(UserRole.user == user.id)
.all()
)
return roles
@mutation.field("updateProfile")
@login_required
async def update_profile(_, info, profile):
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
user = session.query(User).filter(User.id == user_id).one()
if not user:
return {"error": "canoot find user"}
user.update(profile)
session.commit()
return {"error": None, "author": user}
@mutation.field("rateUser")
@login_required
async def rate_user(_, info, rated_userslug, value):
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
rating = (
session.query(UserRating)
.filter(
and_(
UserRating.rater == auth.user_id, UserRating.user == rated_userslug
)
)
.first()
)
if rating:
rating.value = value
session.commit()
return {}
try:
UserRating.create(rater=auth.user_id, user=rated_userslug, value=value)
except Exception as err:
return {"error": err}
return {}
# for mutation.field("follow")
def author_follow(user_id, slug):
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")
def author_unfollow(user_id, slug):
with local_session() as session:
flw = (
session.query(AuthorFollower)
.join(User, User.id == AuthorFollower.author)
.filter(and_(AuthorFollower.follower == user_id, User.slug == slug))
.first()
)
if flw:
session.delete(flw)
session.commit()
return True
return False
@query.field("authorsAll")
async def get_authors_all(_, _info):
q = select(User)
q = add_author_stat_columns(q)
q = q.join(ShoutAuthor, User.id == ShoutAuthor.user)
return get_authors_from_query(q)
@query.field("getAuthorById")
async def get_author_by_id(_, _info, author_id):
q = select(User).where(User.id == author_id)
q = add_author_stat_columns(q)
[author] = get_authors_from_query(q)
with local_session() as session:
comments_count = session.query(Reaction).where(
and_(
Reaction.createdBy == author.id,
Reaction.kind == ReactionKind.COMMENT
)
).count()
author.stat["commented"] = comments_count
return author
@query.field("getAuthor")
async def get_author(_, _info, slug):
q = select(User).where(User.slug == slug)
q = add_author_stat_columns(q)
[author] = get_authors_from_query(q)
with local_session() as session:
comments_count = session.query(Reaction).where(
and_(
Reaction.createdBy == author.id,
Reaction.kind == ReactionKind.COMMENT
)
).count()
author.stat["commented"] = comments_count
return author
@query.field("loadAuthorsBy")
async def load_authors_by(_, info, by, limit, offset):
q = select(User)
q = add_author_stat_columns(q)
if by.get("slug"):
q = q.filter(User.slug.ilike(f"%{by['slug']}%"))
elif by.get("name"):
q = q.filter(User.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("lastSeen"): # in days
days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["lastSeen"])
q = q.filter(User.lastSeen > days_before)
elif by.get("createdAt"): # in days
days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["createdAt"])
q = q.filter(User.createdAt > days_before)
q = q.order_by(by.get("order", User.createdAt)).limit(limit).offset(offset)
return get_authors_from_query(q)

View File

@@ -1,16 +1,14 @@
from datetime import datetime, timedelta, timezone
from sqlalchemy import and_, asc, desc, select, text, func, case
from sqlalchemy.orm import aliased
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from services.exceptions import OperationNotAllowed
from services.db import local_session
from services.schema import mutation, query
from services.presence import notify_reaction
from services.auth import login_required
from base.exceptions import OperationNotAllowed
from base.orm import local_session
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.presence import notify_reaction
from orm.author import Author
def add_reaction_stat_columns(q):
@@ -41,7 +39,7 @@ def add_reaction_stat_columns(q):
return q
def reactions_follow(user_id, shout_id: int, auto=False):
def reactions_follow(author_id, shout_id: int, auto=False):
try:
with local_session() as session:
shout = session.query(Shout).where(Shout.id == shout_id).one()
@@ -50,7 +48,7 @@ def reactions_follow(user_id, shout_id: int, auto=False):
session.query(ShoutReactionsFollower)
.where(
and_(
ShoutReactionsFollower.follower == user_id,
ShoutReactionsFollower.follower == author_id,
ShoutReactionsFollower.shout == shout.id,
)
)
@@ -59,7 +57,7 @@ def reactions_follow(user_id, shout_id: int, auto=False):
if not following:
following = ShoutReactionsFollower.create(
follower=user_id, shout=shout.id, auto=auto
follower=author_id, shout=shout.id, auto=auto
)
session.add(following)
session.commit()
@@ -68,7 +66,7 @@ def reactions_follow(user_id, shout_id: int, auto=False):
return False
def reactions_unfollow(user_id: int, shout_id: int):
def reactions_unfollow(author_id: int, shout_id: int):
try:
with local_session() as session:
shout = session.query(Shout).where(Shout.id == shout_id).one()
@@ -77,7 +75,7 @@ def reactions_unfollow(user_id: int, shout_id: int):
session.query(ShoutReactionsFollower)
.where(
and_(
ShoutReactionsFollower.follower == user_id,
ShoutReactionsFollower.follower == author_id,
ShoutReactionsFollower.shout == shout.id,
)
)
@@ -93,31 +91,31 @@ def reactions_unfollow(user_id: int, shout_id: int):
return False
def is_published_author(session, user_id):
"""checks if user has at least one publication"""
def is_published_author(session, author_id):
"""checks if author has at least one publication"""
return (
session.query(Shout)
.where(Shout.authors.contains(user_id))
.where(Shout.authors.contains(author_id))
.filter(and_(Shout.publishedAt.is_not(None), Shout.deletedAt.is_(None)))
.count()
> 0
)
def check_to_publish(session, user_id, reaction):
def check_to_publish(session, author_id, reaction):
"""set shout to public if publicated approvers amount > 4"""
if not reaction.replyTo and reaction.kind in [
ReactionKind.ACCEPT,
ReactionKind.LIKE,
ReactionKind.PROOF,
]:
if is_published_author(user_id):
if is_published_author(author_id):
# now count how many approvers are voted already
approvers_reactions = (
session.query(Reaction).where(Reaction.shout == reaction.shout).all()
)
approvers = [
user_id,
author_id,
]
for ar in approvers_reactions:
a = ar.createdBy
@@ -128,14 +126,14 @@ def check_to_publish(session, user_id, reaction):
return False
def check_to_hide(session, user_id, reaction):
def check_to_hide(session, reaction):
"""hides any shout if 20% of reactions are negative"""
if not reaction.replyTo and reaction.kind in [
ReactionKind.REJECT,
ReactionKind.DISLIKE,
ReactionKind.DISPROOF,
]:
# if is_published_author(user):
# if is_published_author(author_id):
approvers_reactions = (
session.query(Reaction).where(Reaction.shout == reaction.shout).all()
)
@@ -170,12 +168,10 @@ def set_hidden(session, shout_id):
@mutation.field("createReaction")
@login_required
async def create_reaction(_, info, reaction):
auth: AuthCredentials = info.context["request"].auth
reaction["createdBy"] = auth.user_id
rdict = {}
author_id = info.context["author_id"]
with local_session() as session:
reaction["createdBy"] = author_id
shout = session.query(Shout).where(Shout.id == reaction["shout"]).one()
author = session.query(User).where(User.id == auth.user_id).one()
if reaction["kind"] in [ReactionKind.DISLIKE.name, ReactionKind.LIKE.name]:
existing_reaction = (
@@ -183,7 +179,7 @@ async def create_reaction(_, info, reaction):
.where(
and_(
Reaction.shout == reaction["shout"],
Reaction.createdBy == auth.user_id,
Reaction.createdBy == author_id,
Reaction.kind == reaction["kind"],
Reaction.replyTo == reaction.get("replyTo"),
)
@@ -204,7 +200,7 @@ async def create_reaction(_, info, reaction):
.where(
and_(
Reaction.shout == reaction["shout"],
Reaction.createdBy == auth.user_id,
Reaction.createdBy == author_id,
Reaction.kind == opposite_reaction_kind,
Reaction.replyTo == reaction.get("replyTo"),
)
@@ -218,12 +214,10 @@ async def create_reaction(_, info, reaction):
r = Reaction.create(**reaction)
# Proposal accepting logix
# FIXME: will break if there will be 2 proposals
# FIXME: will break if shout will be changed
if (
r.replyTo is not None
and r.kind == ReactionKind.ACCEPT
and auth.user_id in shout.dict()["authors"]
and author_id in shout.dict()["authors"]
):
replied_reaction = (
session.query(Reaction).where(Reaction.id == r.replyTo).first()
@@ -240,36 +234,37 @@ async def create_reaction(_, info, reaction):
session.add(r)
session.commit()
notify_reaction(r.dict())
rdict = r.dict()
rdict["shout"] = shout.dict()
author = session.query(Author).where(Author.id == author_id).first()
rdict["createdBy"] = author.dict()
# self-regulation mechanics
if check_to_hide(session, auth.user_id, r):
if check_to_hide(session, r):
set_hidden(session, r.shout)
elif check_to_publish(session, auth.user_id, r):
elif check_to_publish(session, author_id, r):
set_published(session, r.shout)
try:
reactions_follow(auth.user_id, reaction["shout"], True)
except Exception as e:
print(f"[resolvers.reactions] error on reactions autofollowing: {e}")
try:
reactions_follow(author_id, reaction["shout"], True)
except Exception as e:
print(f"[resolvers.reactions] error on reactions auto following: {e}")
rdict["stat"] = {"commented": 0, "reacted": 0, "rating": 0}
return {"reaction": rdict}
rdict["stat"] = {"commented": 0, "reacted": 0, "rating": 0}
# notification call
notify_reaction(rdict)
return {"reaction": rdict}
@mutation.field("updateReaction")
@login_required
async def update_reaction(_, info, id, reaction={}):
auth: AuthCredentials = info.context["request"].auth
async def update_reaction(_, info, rid, reaction={}):
author_id = info.context["author_id"]
with local_session() as session:
user = session.query(User).where(User.id == auth.user_id).first()
q = select(Reaction).filter(Reaction.id == id)
q = select(Reaction).filter(Reaction.id == rid)
q = add_reaction_stat_columns(q)
q = q.group_by(Reaction.id)
@@ -279,7 +274,7 @@ async def update_reaction(_, info, id, reaction={}):
if not r:
return {"error": "invalid reaction id"}
if r.createdBy != user.id:
if r.createdBy != author_id:
return {"error": "access denied"}
r.body = reaction["body"]
@@ -296,19 +291,20 @@ async def update_reaction(_, info, id, reaction={}):
"rating": rating_stat,
}
return {"reaction": r}
notify_reaction(r.dict(), "update")
return {"reaction": r}
@mutation.field("deleteReaction")
@login_required
async def delete_reaction(_, info, id):
auth: AuthCredentials = info.context["request"].auth
async def delete_reaction(_, info, rid):
author_id = info.context["author_id"]
with local_session() as session:
r = session.query(Reaction).filter(Reaction.id == id).first()
r = session.query(Reaction).filter(Reaction.id == rid).first()
if not r:
return {"error": "invalid reaction id"}
if r.createdBy != auth.user_id:
if r.createdBy != author_id:
return {"error": "access denied"}
if r.kind in [ReactionKind.LIKE, ReactionKind.DISLIKE]:
@@ -316,12 +312,16 @@ async def delete_reaction(_, info, id):
else:
r.deletedAt = datetime.now(tz=timezone.utc)
session.commit()
notify_reaction(r.dict(), "delete")
return {"reaction": r}
@query.field("loadReactionsBy")
async def load_reactions_by(_, _info, by, limit=50, offset=0):
async def load_reactions_by(_, info, by, limit=50, offset=0):
"""
:param info: graphql meta
:param by: {
:shout - filter by slug
:shouts - filer by shout slug list
@@ -338,8 +338,8 @@ async def load_reactions_by(_, _info, by, limit=50, offset=0):
"""
q = (
select(Reaction, User, Shout)
.join(User, Reaction.createdBy == User.id)
select(Reaction, Author, Shout)
.join(Author, Reaction.createdBy == Author.id)
.join(Shout, Reaction.shout == Shout.id)
)
@@ -349,7 +349,7 @@ async def load_reactions_by(_, _info, by, limit=50, offset=0):
q = q.filter(Shout.slug.in_(by["shouts"]))
if by.get("createdBy"):
q = q.filter(User.slug == by.get("createdBy"))
q = q.filter(Author.id == by.get("createdBy"))
if by.get("topic"):
# TODO: check
@@ -363,42 +363,53 @@ async def load_reactions_by(_, _info, by, limit=50, offset=0):
if by.get("days"):
after = datetime.now(tz=timezone.utc) - timedelta(days=int(by["days"]) or 30)
q = q.filter(Reaction.createdAt > after)
q = q.filter(Reaction.createdAt > after) # FIXME: use comparing operator?
order_way = asc if by.get("sort", "").startswith("-") else desc
order_field = by.get("sort", "").replace("-", "") or Reaction.createdAt
q = q.group_by(Reaction.id, User.id, Shout.id).order_by(order_way(order_field))
q = q.group_by(Reaction.id, Author.id, Shout.id).order_by(order_way(order_field))
q = add_reaction_stat_columns(q)
q = q.where(Reaction.deletedAt.is_(None))
q = q.limit(limit).offset(offset)
reactions = []
with local_session() as session:
for [
reaction,
user,
shout,
reacted_stat,
commented_stat,
rating_stat,
] in session.execute(q):
reaction.createdBy = user
reaction.shout = shout
reaction.stat = {
"rating": rating_stat,
"commented": commented_stat,
"reacted": reacted_stat,
}
reaction.kind = reaction.kind.name
reactions.append(reaction)
session = info.context["session"]
for [
reaction,
author,
shout,
reacted_stat,
commented_stat,
rating_stat,
] in session.execute(q):
reaction.createdBy = author
reaction.shout = shout
reaction.stat = {
"rating": rating_stat,
"commented": commented_stat,
"reacted": reacted_stat,
}
reaction.kind = reaction.kind.name
reactions.append(reaction)
# ?
if by.get("stat"):
reactions.sort(lambda r: r.stat.get(by["stat"]) or r.createdAt)
return reactions
@login_required
@query.field("followedReactions")
async def followed_reactions(_, info):
author_id = info.context["author_id"]
# FIXME: method should return array of shouts
with local_session() as session:
author = session.query(Author).where(Author.id == author_id).first()
reactions = (
session.query(Reaction.shout)
.where(Reaction.createdBy == author.id)
.filter(Reaction.createdAt > author.lastSeen)
.all()
)
return reactions

View File

@@ -1,24 +1,15 @@
from datetime import datetime, timedelta, timezone
from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.sql.expression import (
desc,
asc,
select,
func,
case,
and_,
# text,
nulls_last,
)
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from aiohttp.web_exceptions import HTTPException
from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.sql.expression import desc, asc, select, func, case, and_, nulls_last
from services.auth import login_required
from services.db import local_session
from services.schema import query
from orm import TopicFollower
from orm.topic import TopicFollower
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.user import AuthorFollower
from orm.author import AuthorFollower
def add_stat_columns(q):
@@ -55,9 +46,9 @@ def add_stat_columns(q):
return q
def apply_filters(q, filters, user_id=None):
if filters.get("reacted") and user_id:
q.join(Reaction, Reaction.createdBy == user_id)
def apply_filters(q, filters, author_id=None):
if filters.get("reacted") and author_id:
q.join(Reaction, Reaction.createdBy == author_id)
v = filters.get("visibility")
if v == "public":
@@ -67,8 +58,6 @@ def apply_filters(q, filters, user_id=None):
if filters.get("layout"):
q = q.filter(Shout.layout == filters.get("layout"))
if filters.get("excludeLayout"):
q = q.filter(Shout.layout != filters.get("excludeLayout"))
if filters.get("author"):
q = q.filter(Shout.authors.any(slug=filters.get("author")))
if filters.get("topic"):
@@ -86,8 +75,7 @@ def apply_filters(q, filters, user_id=None):
return q
@query.field("loadShout")
async def load_shout(_, info, slug=None, shout_id=None):
async def load_shout(_, _info, slug=None, shout_id=None):
with local_session() as session:
q = select(Shout).options(
joinedload(Shout.authors),
@@ -103,15 +91,14 @@ async def load_shout(_, info, slug=None, shout_id=None):
q = q.filter(Shout.deletedAt.is_(None)).group_by(Shout.id)
resp = session.execute(q).first()
if resp:
try:
[
shout,
reacted_stat,
commented_stat,
rating_stat,
last_comment,
] = resp
_last_comment,
] = session.execute(q).first()
shout.stat = {
"viewed": shout.views,
@@ -124,21 +111,20 @@ async def load_shout(_, info, slug=None, shout_id=None):
session.query(ShoutAuthor).join(Shout).where(Shout.slug == slug)
):
for author in shout.authors:
if author.id == author_caption.user:
if author.id == author_caption.author:
author.caption = author_caption.caption
return shout
else:
print("Slug was not found: %s" % slug)
return
except Exception:
raise HTTPException(status_code=404, detail="Slug was not found: %s" % slug)
@query.field("loadShouts")
async def load_shouts_by(_, info, options):
"""
:param _:
:param info:GraphQLInfo
:param options: {
filters: {
layout: 'music',
excludeLayout: 'article',
layout: 'audio',
visibility: "public",
author: 'discours',
topic: 'culture',
@@ -161,13 +147,13 @@ async def load_shouts_by(_, info, options):
joinedload(Shout.authors),
joinedload(Shout.topics),
)
.where(and_(Shout.deletedAt.is_(None), Shout.layout.is_not(None)))
.where(Shout.deletedAt.is_(None))
)
q = add_stat_columns(q)
auth: AuthCredentials = info.context["request"].auth
q = apply_filters(q, options.get("filters", {}), auth.user_id)
author_id = info.context["author_id"]
q = apply_filters(q, options.get("filters", {}), author_id)
order_by = options.get("order_by", Shout.publishedAt)
@@ -185,15 +171,14 @@ async def load_shouts_by(_, info, options):
)
shouts = []
shouts_map = {}
with local_session() as session:
shouts_map = {}
for [
shout,
reacted_stat,
commented_stat,
rating_stat,
last_comment,
_last_comment,
] in session.execute(q).unique():
shouts.append(shout)
shout.stat = {
@@ -207,87 +192,59 @@ async def load_shouts_by(_, info, options):
return shouts
@query.field("loadDrafts")
@login_required
async def get_drafts(_, info):
auth: AuthCredentials = info.context["request"].auth
user_id = auth.user_id
q = (
select(Shout)
.options(
joinedload(Shout.authors),
joinedload(Shout.topics),
)
.where(and_(Shout.deletedAt.is_(None), Shout.createdBy == user_id))
)
q = q.group_by(Shout.id)
shouts = []
with local_session() as session:
for [shout] in session.execute(q).unique():
shouts.append(shout)
return shouts
@query.field("myFeed")
@login_required
async def get_my_feed(_, info, options):
auth: AuthCredentials = info.context["request"].auth
user_id = auth.user_id
subquery = (
select(Shout.id)
.join(ShoutAuthor)
.join(AuthorFollower, AuthorFollower.follower == user_id)
.join(ShoutTopic)
.join(TopicFollower, TopicFollower.follower == user_id)
)
q = (
select(Shout)
.options(
joinedload(Shout.authors),
joinedload(Shout.topics),
author_id = info.context["author_id"]
with local_session() as session:
subquery = (
select(Shout.id)
.join(ShoutAuthor)
.join(AuthorFollower, AuthorFollower.follower._is(author_id))
.join(ShoutTopic)
.join(TopicFollower, TopicFollower.follower._is(author_id))
)
.where(
and_(
Shout.publishedAt.is_not(None),
Shout.deletedAt.is_(None),
Shout.id.in_(subquery),
q = (
select(Shout)
.options(
joinedload(Shout.authors),
joinedload(Shout.topics),
)
.where(
and_(
Shout.publishedAt.is_not(None),
Shout.deletedAt.is_(None),
Shout.id.in_(subquery),
)
)
)
)
q = add_stat_columns(q)
q = apply_filters(q, options.get("filters", {}), user_id)
q = add_stat_columns(q)
q = apply_filters(q, options.get("filters", {}), author_id)
order_by = options.get("order_by", Shout.publishedAt)
order_by = options.get("order_by", Shout.publishedAt)
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)
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(nulls_last(query_order_by))
.limit(limit)
.offset(offset)
)
q = (
q.group_by(Shout.id)
.order_by(nulls_last(query_order_by))
.limit(limit)
.offset(offset)
)
shouts = []
with local_session() as session:
shouts = []
shouts_map = {}
for [
shout,
reacted_stat,
commented_stat,
rating_stat,
last_comment,
_last_comment,
] in session.execute(q).unique():
shouts.append(shout)
shout.stat = {
@@ -297,5 +254,5 @@ async def get_my_feed(_, info, options):
"rating": rating_stat,
}
shouts_map[shout.id] = shout
# FIXME: shouts_map does not go anywhere?
return shouts

View File

@@ -1,12 +1,22 @@
from sqlalchemy import and_, select, distinct, func
from sqlalchemy.orm import aliased
from auth.authenticate import login_required
from services.auth import login_required
from services.db import local_session
from services.schema import mutation, query
from resolvers import mutation, query
from orm.shout import ShoutTopic, ShoutAuthor
from orm.topic import Topic, TopicFollower
from orm import User
from orm.author import Author
async def followed_topics(follower_id):
q = select(Author)
q = add_topic_stat_columns(q)
q = q.join(TopicFollower, TopicFollower.author == Author.id).where(
TopicFollower.follower == follower_id
)
# Pass the query to the get_authors_from_query function and return the results
return get_topics_from_query(q)
def add_topic_stat_columns(q):
@@ -54,10 +64,10 @@ def get_topics_from_query(q):
return topics
def followed_by_user(user_id):
def topics_followed_by(author_id):
q = select(Topic)
q = add_topic_stat_columns(q)
q = q.join(TopicFollower).where(TopicFollower.follower == user_id)
q = q.join(TopicFollower).where(TopicFollower.follower == author_id)
return get_topics_from_query(q)
@@ -79,10 +89,10 @@ async def topics_by_community(_, info, community):
@query.field("topicsByAuthor")
async def topics_by_author(_, _info, author):
async def topics_by_author(_, _info, author_id):
q = select(Topic)
q = add_topic_stat_columns(q)
q = q.join(User).where(User.slug == author)
q = q.join(Author).where(Author.id == author_id)
return get_topics_from_query(q)
@@ -108,7 +118,6 @@ async def create_topic(_, _info, inp):
return {"topic": new_topic}
@mutation.field("updateTopic")
@login_required
async def update_topic(_, _info, inp):
slug = inp["slug"]
@@ -123,16 +132,16 @@ async def update_topic(_, _info, inp):
return {"topic": topic}
def topic_follow(user_id, slug):
def topic_follow(follower_id, slug):
try:
with local_session() as session:
topic = session.query(Topic).where(Topic.slug == slug).one()
following = TopicFollower.create(topic=topic.id, follower=user_id)
following = TopicFollower.create(topic=topic.id, follower=follower_id)
session.add(following)
session.commit()
return True
except:
except Exception:
return False
@@ -149,12 +158,11 @@ def topic_unfollow(user_id, slug):
session.delete(sub)
session.commit()
return True
except:
except Exception:
pass
return False
@query.field("topicsRandom")
async def topics_random(_, info, amount=12):
q = select(Topic)
q = q.join(ShoutTopic)

View File

@@ -1,56 +0,0 @@
import os
import shutil
import tempfile
import uuid
import boto3
from botocore.exceptions import BotoCoreError, ClientError
from starlette.responses import JSONResponse
STORJ_ACCESS_KEY = os.environ.get('STORJ_ACCESS_KEY')
STORJ_SECRET_KEY = os.environ.get('STORJ_SECRET_KEY')
STORJ_END_POINT = os.environ.get('STORJ_END_POINT')
STORJ_BUCKET_NAME = os.environ.get('STORJ_BUCKET_NAME')
CDN_DOMAIN = os.environ.get('CDN_DOMAIN')
async def upload_handler(request):
form = await request.form()
file = form.get('file')
if file is None:
return JSONResponse({'error': 'No file uploaded'}, status_code=400)
file_name, file_extension = os.path.splitext(file.filename)
key = str(uuid.uuid4()) + file_extension
# Create an S3 client with Storj configuration
s3 = boto3.client('s3',
aws_access_key_id=STORJ_ACCESS_KEY,
aws_secret_access_key=STORJ_SECRET_KEY,
endpoint_url=STORJ_END_POINT)
try:
# Save the uploaded file to a temporary file
with tempfile.NamedTemporaryFile() as tmp_file:
shutil.copyfileobj(file.file, tmp_file)
s3.upload_file(
Filename=tmp_file.name,
Bucket=STORJ_BUCKET_NAME,
Key=key,
ExtraArgs={
"ContentType": file.content_type
}
)
url = 'http://' + CDN_DOMAIN + '/' + key
return JSONResponse({'url': url, 'originalFilename': file.filename})
except (BotoCoreError, ClientError) as e:
print(e)
return JSONResponse({'error': 'Failed to upload file'}, status_code=500)